mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-06 23:43:30 +00:00
initial commit
initial commit
This commit is contained in:
3
voicemeeterlib/__init__.py
Normal file
3
voicemeeterlib/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from .factory import request_remote_obj as api
|
||||
|
||||
__ALL__ = ["api"]
|
||||
284
voicemeeterlib/base.py
Normal file
284
voicemeeterlib/base.py
Normal file
@@ -0,0 +1,284 @@
|
||||
import ctypes as ct
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from functools import partial
|
||||
from threading import Thread
|
||||
from typing import Iterable, NoReturn, Optional, Self, Union
|
||||
|
||||
from .cbindings import CBindings
|
||||
from .error import VMError
|
||||
from .kinds import KindId
|
||||
from .subject import Subject
|
||||
from .util import polling, script
|
||||
|
||||
|
||||
class Remote(CBindings):
|
||||
"""Base class responsible for wrapping the C Remote API"""
|
||||
|
||||
DELAY = 0.001
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.cache = {}
|
||||
self.subject = Subject()
|
||||
self._strip_levels, self._bus_levels = self.all_levels
|
||||
|
||||
for attr, val in kwargs.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
"""setup procedures"""
|
||||
self.login()
|
||||
self.init_thread()
|
||||
return self
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
|
||||
def init_thread(self):
|
||||
"""Starts updates thread."""
|
||||
self.running = True
|
||||
t = Thread(target=self._updates, daemon=True)
|
||||
t.start()
|
||||
|
||||
def _updates(self):
|
||||
"""Continously update observers of dirty states."""
|
||||
while self.running:
|
||||
if self.pdirty:
|
||||
self.subject.notify("pdirty")
|
||||
if self.ldirty:
|
||||
self._strip_levels = self.strip_buf
|
||||
self._bus_levels = self.bus_buf
|
||||
self.subject.notify(
|
||||
"ldirty",
|
||||
(
|
||||
self._strip_levels,
|
||||
self._strip_comp,
|
||||
self._bus_levels,
|
||||
self._bus_comp,
|
||||
),
|
||||
)
|
||||
time.sleep(self.ratelimit)
|
||||
|
||||
def login(self) -> NoReturn:
|
||||
"""Login to the API, initialize dirty parameters"""
|
||||
res = self.vm_login()
|
||||
if res == 0:
|
||||
print(f"Successfully logged into {self}")
|
||||
elif res == 1:
|
||||
self.run_voicemeeter(self.kind.name)
|
||||
self.clear_dirty()
|
||||
|
||||
def run_voicemeeter(self, kind_id: str) -> NoReturn:
|
||||
if kind_id not in (kind.name.lower() for kind in KindId):
|
||||
raise VMError(f"Unexpected Voicemeeter type: '{kind_id}'")
|
||||
if kind_id == "potato" and ct.sizeof(ct.c_voidp) == 8:
|
||||
value = KindId[kind_id.upper()].value + 3
|
||||
else:
|
||||
value = KindId[kind_id.upper()].value
|
||||
self.vm_runvm(value)
|
||||
time.sleep(1)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
|
||||
type_ = ct.c_long()
|
||||
self.vm_get_type(ct.byref(type_))
|
||||
return KindId(type_.value).name.lower()
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
ver = ct.c_long()
|
||||
self.vm_get_version(ct.byref(ver))
|
||||
v1 = (ver.value & 0xFF000000) >> 24
|
||||
v2 = (ver.value & 0x00FF0000) >> 16
|
||||
v3 = (ver.value & 0x0000FF00) >> 8
|
||||
v4 = ver.value & 0x000000FF
|
||||
return f"{v1}.{v2}.{v3}.{v4}"
|
||||
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
"""True iff UI parameters have been updated."""
|
||||
return self.vm_pdirty() == 1
|
||||
|
||||
@property
|
||||
def mdirty(self) -> bool:
|
||||
"""True iff MB parameters have been updated."""
|
||||
return self.vm_mdirty() == 1
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
"""True iff levels have been updated."""
|
||||
self.strip_buf, self.bus_buf = self.all_levels
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(not a == b for a, b in zip(self.strip_buf, self._strip_levels)),
|
||||
tuple(not a == b for a, b in zip(self.bus_buf, self._bus_levels)),
|
||||
)
|
||||
return any(
|
||||
any(l)
|
||||
for l in (
|
||||
self._strip_comp,
|
||||
self._bus_comp,
|
||||
)
|
||||
)
|
||||
|
||||
def clear_dirty(self):
|
||||
while self.pdirty or self.mdirty:
|
||||
pass
|
||||
|
||||
@polling
|
||||
def get(self, param: str, is_string: Optional[bool] = False) -> Union[str, float]:
|
||||
"""Gets a string or float parameter"""
|
||||
if is_string:
|
||||
buf = ct.create_unicode_buffer(512)
|
||||
self.call(
|
||||
partial(self.vm_get_parameter_string, param.encode(), ct.byref(buf))
|
||||
)
|
||||
else:
|
||||
buf = ct.c_float()
|
||||
self.call(
|
||||
partial(self.vm_get_parameter_float, param.encode(), ct.byref(buf))
|
||||
)
|
||||
return buf.value
|
||||
|
||||
def set(self, param: str, val: Union[str, float]) -> NoReturn:
|
||||
"""Sets a string or float parameter. Caches value"""
|
||||
if isinstance(val, str):
|
||||
if len(val) >= 512:
|
||||
raise VMError("String is too long")
|
||||
self.call(
|
||||
partial(self.vm_set_parameter_string, param.encode(), ct.c_wchar_p(val))
|
||||
)
|
||||
else:
|
||||
self.call(
|
||||
partial(
|
||||
self.vm_set_parameter_float, param.encode(), ct.c_float(float(val))
|
||||
)
|
||||
)
|
||||
self.cache[param] = val
|
||||
|
||||
@polling
|
||||
def get_buttonstatus(self, id: int, mode: int) -> int:
|
||||
"""Gets a macrobutton parameter"""
|
||||
state = ct.c_float()
|
||||
self.call(
|
||||
partial(
|
||||
self.vm_get_buttonstatus,
|
||||
ct.c_long(id),
|
||||
ct.byref(state),
|
||||
ct.c_long(mode),
|
||||
)
|
||||
)
|
||||
return int(state.value)
|
||||
|
||||
def set_buttonstatus(self, id: int, state: int, mode: int) -> NoReturn:
|
||||
"""Sets a macrobutton parameter. Caches value"""
|
||||
c_state = ct.c_float(float(state))
|
||||
self.call(
|
||||
partial(self.vm_set_buttonstatus, ct.c_long(id), c_state, ct.c_long(mode))
|
||||
)
|
||||
self.cache[f"mb_{id}_{mode}"] = int(c_state.value)
|
||||
|
||||
def get_num_devices(self, direction: str = None) -> int:
|
||||
"""Retrieves number of physical devices connected"""
|
||||
if direction not in ("in", "out"):
|
||||
raise VMError("Expected a direction: in or out")
|
||||
func = getattr(self, f"vm_get_num_{direction}devices")
|
||||
return func()
|
||||
|
||||
def get_device_description(self, index: int, direction: str = None) -> tuple:
|
||||
"""Returns a tuple of device parameters"""
|
||||
if direction not in ("in", "out"):
|
||||
raise VMError("Expected a direction: in or out")
|
||||
type_ = ct.c_long()
|
||||
name = ct.create_unicode_buffer(256)
|
||||
hwid = ct.create_unicode_buffer(256)
|
||||
func = getattr(self, f"vm_get_desc_{direction}devices")
|
||||
func(
|
||||
ct.c_long(index),
|
||||
ct.byref(type_),
|
||||
ct.byref(name),
|
||||
ct.byref(hwid),
|
||||
)
|
||||
return (name.value, type_.value, hwid.value)
|
||||
|
||||
@property
|
||||
def all_levels(self) -> Iterable:
|
||||
"""
|
||||
returns both level arrays (strip_levels, bus_levels) BEFORE math conversion
|
||||
|
||||
strip levels in PREFADER mode.
|
||||
"""
|
||||
return (
|
||||
tuple(
|
||||
self.get_level(0, i)
|
||||
for i in range(2 * self.kind.phys_in + 8 * self.kind.virt_in)
|
||||
),
|
||||
tuple(
|
||||
self.get_level(3, i)
|
||||
for i in range(8 * (self.kind.phys_out + self.kind.virt_out))
|
||||
),
|
||||
)
|
||||
|
||||
def get_level(self, type_: int, index: int) -> float:
|
||||
"""Retrieves a single level value"""
|
||||
val = ct.c_float()
|
||||
self.vm_get_level(ct.c_long(type_), ct.c_long(index), ct.byref(val))
|
||||
return val.value
|
||||
|
||||
@script
|
||||
def sendtext(self, script: str):
|
||||
"""Sets many parameters from a script"""
|
||||
if len(script) > 48000:
|
||||
raise ValueError("Script too large, max size 48kB")
|
||||
self.call(partial(self.vm_set_parameter_multi, script))
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
def apply(self, data: dict):
|
||||
"""
|
||||
Sets all parameters of a dict
|
||||
|
||||
minor delay between each recursion
|
||||
"""
|
||||
|
||||
def param(key):
|
||||
obj, m2, *rem = key.split("-")
|
||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
||||
if obj in ("strip", "bus", "button"):
|
||||
return getattr(self, obj)[index]
|
||||
elif obj == "vban":
|
||||
return getattr(getattr(self, obj), f"{m2}stream")[index]
|
||||
else:
|
||||
raise ValueError(obj)
|
||||
|
||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
||||
|
||||
def apply_config(self, name):
|
||||
"""applies a config from memory"""
|
||||
error_msg = (
|
||||
f"No config with name '{name}' is loaded into memory",
|
||||
f"Known configs: {list(self.configs.keys())}",
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
except KeyError as e:
|
||||
print(("\n").join(error_msg))
|
||||
print(f"Profile '{name}' applied!")
|
||||
|
||||
def logout(self) -> NoReturn:
|
||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
||||
self.clear_dirty()
|
||||
time.sleep(0.1)
|
||||
res = self.vm_logout()
|
||||
if res == 0:
|
||||
print(f"Successfully logged out of {self}")
|
||||
|
||||
def end_thread(self):
|
||||
self.running = False
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> NoReturn:
|
||||
"""teardown procedures"""
|
||||
self.end_thread()
|
||||
self.logout()
|
||||
207
voicemeeterlib/bus.py
Normal file
207
voicemeeterlib/bus.py
Normal file
@@ -0,0 +1,207 @@
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from math import log
|
||||
from typing import Union
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .meta import bus_mode_prop
|
||||
|
||||
|
||||
class Bus(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for bus
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"bus[{self.index}]"
|
||||
|
||||
@property
|
||||
def mute(self) -> bool:
|
||||
return self.getter("mute") == 1
|
||||
|
||||
@mute.setter
|
||||
def mute(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("mute is a boolean parameter")
|
||||
self.setter("mute", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mono(self) -> bool:
|
||||
return self.getter("mono") == 1
|
||||
|
||||
@mono.setter
|
||||
def mono(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("mono is a boolean parameter")
|
||||
self.setter("mono", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def eq(self) -> bool:
|
||||
return self.getter("eq.On") == 1
|
||||
|
||||
@eq.setter
|
||||
def eq(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("eq is a boolean parameter")
|
||||
self.setter("eq.On", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def eq_ab(self) -> bool:
|
||||
return self.getter("eq.ab") == 1
|
||||
|
||||
@eq_ab.setter
|
||||
def eq_ab(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("eq_ab is a boolean parameter")
|
||||
self.setter("eq.ab", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def sel(self) -> bool:
|
||||
return self.getter("sel") == 1
|
||||
|
||||
@sel.setter
|
||||
def sel(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("sel is a boolean parameter")
|
||||
self.setter("sel", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.getter("Label", is_string=True)
|
||||
|
||||
@label.setter
|
||||
def label(self, val: str):
|
||||
if not isinstance(val, str):
|
||||
raise VMError("label is a string parameter")
|
||||
self.setter("Label", val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return round(self.getter("gain"), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@property
|
||||
def device(self) -> str:
|
||||
return self.getter("device.name", is_string=True)
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return int(self.getter("device.sr"))
|
||||
|
||||
|
||||
class VirtualBus(Bus):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
|
||||
class BusLevel(IRemote):
|
||||
def __init__(self, remote, index):
|
||||
super().__init__(remote, index)
|
||||
self.level_map = tuple(
|
||||
(i, i + 8)
|
||||
for i in range(0, (remote.kind.phys_out + remote.kind.virt_out) * 8, 8)
|
||||
)
|
||||
|
||||
def getter(self):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
return round(20 * log(i, 10), 1) if i > 0 else -200.0
|
||||
|
||||
range_ = self.level_map[self.index]
|
||||
return tuple(fget(i) for i in self._remote._bus_levels[range_[0] : range_[-1]])
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}]"
|
||||
|
||||
@property
|
||||
def all(self) -> tuple:
|
||||
return self.getter()
|
||||
|
||||
@property
|
||||
def updated(self) -> tuple:
|
||||
return self._remote._bus_comp
|
||||
|
||||
|
||||
def _make_bus_mode_mixin():
|
||||
"""Creates a mixin of Bus Modes."""
|
||||
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}].mode"
|
||||
|
||||
return type(
|
||||
"BusModeMixin",
|
||||
(IRemote,),
|
||||
{
|
||||
"identifier": property(identifier),
|
||||
**{
|
||||
mode: bus_mode_prop(mode)
|
||||
for mode in [
|
||||
"normal",
|
||||
"amix",
|
||||
"bmix",
|
||||
"repeat",
|
||||
"composite",
|
||||
"tvmix",
|
||||
"upmix21",
|
||||
"upmix41",
|
||||
"upmix61",
|
||||
"centeronly",
|
||||
"lfeonly",
|
||||
"rearonly",
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
"""
|
||||
Factory method for buses
|
||||
|
||||
Returns a physical or virtual bus subclass
|
||||
"""
|
||||
BUS_cls = PhysicalBus if phys_bus else VirtualBus
|
||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||
return type(
|
||||
f"{BUS_cls.__name__}{remote.kind}",
|
||||
(BUS_cls,),
|
||||
{
|
||||
"levels": BusLevel(remote, i),
|
||||
"mode": BUSMODEMIXIN_cls(remote, i),
|
||||
},
|
||||
)(remote, i)
|
||||
|
||||
|
||||
def request_bus_obj(phys_bus, remote, i) -> Bus:
|
||||
"""
|
||||
Bus entry point. Wraps factory method.
|
||||
|
||||
Returns a reference to a bus subclass of a kind
|
||||
"""
|
||||
return bus_factory(phys_bus, remote, i)
|
||||
105
voicemeeterlib/cbindings.py
Normal file
105
voicemeeterlib/cbindings.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import ctypes as ct
|
||||
from abc import ABCMeta
|
||||
from ctypes.wintypes import CHAR, FLOAT, LONG, WCHAR
|
||||
|
||||
from .error import CAPIError
|
||||
from .inst import libc
|
||||
|
||||
|
||||
class CBindings(metaclass=ABCMeta):
|
||||
"""
|
||||
C bindings defined here.
|
||||
|
||||
Maps expected ctype argument and res types for each binding.
|
||||
"""
|
||||
|
||||
vm_login = libc.VBVMR_Login
|
||||
vm_login.restype = LONG
|
||||
vm_login.argtypes = None
|
||||
|
||||
vm_logout = libc.VBVMR_Logout
|
||||
vm_logout.restype = LONG
|
||||
vm_logout.argtypes = None
|
||||
|
||||
vm_runvm = libc.VBVMR_RunVoicemeeter
|
||||
vm_runvm.restype = LONG
|
||||
vm_runvm.argtypes = [LONG]
|
||||
|
||||
vm_get_type = libc.VBVMR_GetVoicemeeterType
|
||||
vm_get_type.restype = LONG
|
||||
vm_get_type.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
vm_get_version = libc.VBVMR_GetVoicemeeterVersion
|
||||
vm_get_version.restype = LONG
|
||||
vm_get_version.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
vm_mdirty = libc.VBVMR_MacroButton_IsDirty
|
||||
vm_mdirty.restype = LONG
|
||||
vm_mdirty.argtypes = None
|
||||
|
||||
vm_get_buttonstatus = libc.VBVMR_MacroButton_GetStatus
|
||||
vm_get_buttonstatus.restype = LONG
|
||||
vm_get_buttonstatus.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||
|
||||
vm_set_buttonstatus = libc.VBVMR_MacroButton_SetStatus
|
||||
vm_set_buttonstatus.restype = LONG
|
||||
vm_set_buttonstatus.argtypes = [LONG, FLOAT, LONG]
|
||||
|
||||
vm_pdirty = libc.VBVMR_IsParametersDirty
|
||||
vm_pdirty.restype = LONG
|
||||
vm_pdirty.argtypes = None
|
||||
|
||||
vm_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||
vm_get_parameter_float.restype = LONG
|
||||
vm_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||
|
||||
vm_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||
vm_set_parameter_float.restype = LONG
|
||||
vm_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||
|
||||
vm_get_parameter_string = libc.VBVMR_GetParameterStringW
|
||||
vm_get_parameter_string.restype = LONG
|
||||
vm_get_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||
|
||||
vm_set_parameter_string = libc.VBVMR_SetParameterStringW
|
||||
vm_set_parameter_string.restype = LONG
|
||||
vm_set_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||
|
||||
vm_set_parameter_multi = libc.VBVMR_SetParameters
|
||||
vm_set_parameter_multi.restype = LONG
|
||||
vm_set_parameter_multi.argtypes = [ct.POINTER(CHAR), ct.POINTER(CHAR)]
|
||||
|
||||
vm_get_level = libc.VBVMR_GetLevel
|
||||
vm_get_level.restype = LONG
|
||||
vm_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||
|
||||
vm_get_num_indevices = libc.VBVMR_Input_GetDeviceNumber
|
||||
vm_get_num_indevices.restype = LONG
|
||||
vm_get_num_indevices.argtypes = None
|
||||
|
||||
vm_get_desc_indevices = libc.VBVMR_Input_GetDeviceDescW
|
||||
vm_get_desc_indevices.restype = LONG
|
||||
vm_get_desc_indevices.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
vm_get_num_outdevices = libc.VBVMR_Output_GetDeviceNumber
|
||||
vm_get_num_outdevices.restype = LONG
|
||||
vm_get_num_outdevices.argtypes = None
|
||||
|
||||
vm_get_desc_outdevices = libc.VBVMR_Output_GetDeviceDescW
|
||||
vm_get_desc_outdevices.restype = LONG
|
||||
vm_get_desc_outdevices.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
def call(self, func):
|
||||
res = func()
|
||||
if res != 0:
|
||||
raise CAPIError(f"Function {func.func.__name__} returned {res}")
|
||||
55
voicemeeterlib/command.py
Normal file
55
voicemeeterlib/command.py
Normal file
@@ -0,0 +1,55 @@
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .meta import action_prop
|
||||
|
||||
|
||||
class Command(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for command
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for command class.
|
||||
|
||||
Returns a Command class of a kind.
|
||||
"""
|
||||
CMD_cls = type(
|
||||
f"Command{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
param: action_prop(param)
|
||||
for param in ["show", "shutdown", "restart"]
|
||||
},
|
||||
"hide": action_prop("show", val=0),
|
||||
},
|
||||
)
|
||||
return CMD_cls(remote)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}"
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return "Command"
|
||||
|
||||
def set_showvbanchat(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("showvbanchat is a boolean parameter")
|
||||
self.setter("DialogShow.VBANCHAT", 1 if val else 0)
|
||||
|
||||
showvbanchat = property(fset=set_showvbanchat)
|
||||
|
||||
def set_lock(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("lock is a boolean parameter")
|
||||
self.setter("lock", 1 if val else 0)
|
||||
|
||||
lock = property(fset=set_lock)
|
||||
|
||||
def reset(self):
|
||||
self._remote.apply_config("reset")
|
||||
191
voicemeeterlib/config.py
Normal file
191
voicemeeterlib/config.py
Normal file
@@ -0,0 +1,191 @@
|
||||
import itertools
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
|
||||
from .kinds import request_kind_map as kindmap
|
||||
|
||||
|
||||
class TOMLStrBuilder:
|
||||
"""builds a config profile, as a string, for the toml parser"""
|
||||
|
||||
def __init__(self, kind):
|
||||
self.kind = kind
|
||||
self.phys_in, self.virt_in = kind.ins
|
||||
self.phys_out, self.virt_out = kind.outs
|
||||
|
||||
self.higher = itertools.chain(
|
||||
[f"strip-{i}" for i in range(kind.num_strip)],
|
||||
[f"bus-{i}" for i in range(kind.num_bus)],
|
||||
)
|
||||
|
||||
def init_config(self, profile=None):
|
||||
self.virt_strip_params = (
|
||||
[
|
||||
"mute = false",
|
||||
"mono = false",
|
||||
"solo = false",
|
||||
"gain = 0.0",
|
||||
]
|
||||
+ [f"A{i} = false" for i in range(1, self.phys_out + 1)]
|
||||
+ [f"B{i} = false" for i in range(1, self.virt_out + 1)]
|
||||
)
|
||||
self.phys_strip_params = self.virt_strip_params + [
|
||||
"comp = 0.0",
|
||||
"gate = 0.0",
|
||||
]
|
||||
self.bus_bool = ["mono = false", "eq = false", "mute = false"]
|
||||
|
||||
if profile == "reset":
|
||||
self.reset_config()
|
||||
|
||||
def reset_config(self):
|
||||
self.phys_strip_params = list(
|
||||
map(lambda x: x.replace("B1 = false", "B1 = true"), self.phys_strip_params)
|
||||
)
|
||||
self.virt_strip_params = list(
|
||||
map(lambda x: x.replace("A1 = false", "A1 = true"), self.virt_strip_params)
|
||||
)
|
||||
|
||||
def build(self, profile="reset"):
|
||||
self.init_config(profile)
|
||||
toml_str = str()
|
||||
for eachclass in self.higher:
|
||||
toml_str += f"[{eachclass}]\n"
|
||||
toml_str = self.join(eachclass, toml_str)
|
||||
return toml_str
|
||||
|
||||
def join(self, eachclass, toml_str):
|
||||
kls, index = eachclass.split("-")
|
||||
match kls:
|
||||
case "strip":
|
||||
toml_str += ("\n").join(
|
||||
self.phys_strip_params
|
||||
if int(index) < self.phys_in
|
||||
else self.virt_strip_params
|
||||
)
|
||||
case "bus":
|
||||
toml_str += ("\n").join(self.bus_bool)
|
||||
case _:
|
||||
pass
|
||||
return toml_str + "\n"
|
||||
|
||||
|
||||
class TOMLDataExtractor:
|
||||
def __init__(self, file):
|
||||
self._data = dict()
|
||||
with open(file, "rb") as f:
|
||||
self._data = tomllib.load(f)
|
||||
|
||||
@property
|
||||
def data(self):
|
||||
return self._data
|
||||
|
||||
@data.setter
|
||||
def data(self, value):
|
||||
self._data = value
|
||||
|
||||
|
||||
def dataextraction_factory(file):
|
||||
"""
|
||||
factory function for parser
|
||||
|
||||
this opens the possibility for other parsers to be added
|
||||
"""
|
||||
if file.suffix == ".toml":
|
||||
extractor = TOMLDataExtractor
|
||||
else:
|
||||
raise ValueError("Cannot extract data from {}".format(file))
|
||||
return extractor(file)
|
||||
|
||||
|
||||
class SingletonType(type):
|
||||
"""ensure only a single instance of Loader object"""
|
||||
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
class Loader(metaclass=SingletonType):
|
||||
"""
|
||||
invokes the parser
|
||||
|
||||
checks if config already in memory
|
||||
|
||||
loads data into memory if not found
|
||||
"""
|
||||
|
||||
def __init__(self, kind):
|
||||
self._kind = kind
|
||||
self._configs = dict()
|
||||
self.defaults(kind)
|
||||
self.parser = None
|
||||
|
||||
def defaults(self, kind):
|
||||
self.builder = TOMLStrBuilder(kind)
|
||||
toml_str = self.builder.build()
|
||||
self.register("reset", tomllib.loads(toml_str))
|
||||
|
||||
def parse(self, identifier, data):
|
||||
if identifier in self._configs:
|
||||
print(f"config file with name {identifier} already in memory, skipping..")
|
||||
return False
|
||||
self.parser = dataextraction_factory(data)
|
||||
return True
|
||||
|
||||
def register(self, identifier, data=None):
|
||||
self._configs[identifier] = data if data else self.parser.data
|
||||
print(f"config {self.name}/{identifier} loaded into memory")
|
||||
|
||||
def deregister(self):
|
||||
self._configs.clear()
|
||||
self.defaults(self._kind)
|
||||
|
||||
@property
|
||||
def configs(self):
|
||||
return self._configs
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._kind.name
|
||||
|
||||
|
||||
def loader(kind):
|
||||
"""
|
||||
traverses defined paths for config files
|
||||
|
||||
directs the loader
|
||||
|
||||
returns configs loaded into memory
|
||||
"""
|
||||
loader = Loader(kind)
|
||||
|
||||
for path in (
|
||||
Path.cwd() / "configs" / kind.name,
|
||||
Path(__file__).parent / "configs" / kind.name,
|
||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||
):
|
||||
if path.is_dir():
|
||||
print(f"Checking [{path}] for TOML config files:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
if loader.parse(identifier, file):
|
||||
loader.register(identifier)
|
||||
return loader.configs
|
||||
|
||||
|
||||
def request_config(kind_id: str):
|
||||
"""
|
||||
config entry point.
|
||||
|
||||
Returns all configs loaded into memory for a kind
|
||||
"""
|
||||
try:
|
||||
configs = loader(kindmap(kind_id))
|
||||
except KeyError as e:
|
||||
print(f"Unknown Voicemeeter kind '{kind_id}'")
|
||||
return configs
|
||||
72
voicemeeterlib/device.py
Normal file
72
voicemeeterlib/device.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from abc import abstractmethod
|
||||
from typing import Union
|
||||
|
||||
from .iremote import IRemote
|
||||
|
||||
|
||||
class Adapter(IRemote):
|
||||
"""Adapter to the common interface."""
|
||||
|
||||
@abstractmethod
|
||||
def ins(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def outs(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def input(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def output(self):
|
||||
pass
|
||||
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
def getter(self, index: int = None, direction: str = None) -> Union[int, dict]:
|
||||
if index is None:
|
||||
return self._remote.get_num_devices(direction)
|
||||
|
||||
vals = self._remote.get_device_description(index, direction)
|
||||
types = {1: "mme", 3: "wdm", 4: "ks", 5: "asio"}
|
||||
return {"name": vals[0], "type": types[vals[1]], "id": vals[2]}
|
||||
|
||||
|
||||
class Device(Adapter):
|
||||
"""Defines concrete implementation for device"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for device.
|
||||
|
||||
Returns a Device class of a kind.
|
||||
"""
|
||||
|
||||
def num_ins(cls) -> int:
|
||||
return cls.getter(direction="in")
|
||||
|
||||
def num_outs(cls) -> int:
|
||||
return cls.getter(direction="out")
|
||||
|
||||
DEVICE_cls = type(
|
||||
f"Device{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
"ins": property(num_ins),
|
||||
"outs": property(num_outs),
|
||||
},
|
||||
)
|
||||
return DEVICE_cls(remote)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}"
|
||||
|
||||
def input(self, index: int) -> dict:
|
||||
return self.getter(index=index, direction="in")
|
||||
|
||||
def output(self, index: int) -> dict:
|
||||
return self.getter(index=index, direction="out")
|
||||
16
voicemeeterlib/error.py
Normal file
16
voicemeeterlib/error.py
Normal file
@@ -0,0 +1,16 @@
|
||||
class InstallError(Exception):
|
||||
"""errors related to installation"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class CAPIError(Exception):
|
||||
"""errors related to low-level C API calls"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class VMError(Exception):
|
||||
"""general errors"""
|
||||
|
||||
pass
|
||||
211
voicemeeterlib/factory.py
Normal file
211
voicemeeterlib/factory.py
Normal file
@@ -0,0 +1,211 @@
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from functools import cached_property
|
||||
from typing import Iterable, NoReturn, Self
|
||||
|
||||
from .base import Remote
|
||||
from .bus import request_bus_obj as bus
|
||||
from .command import Command
|
||||
from .config import request_config as configs
|
||||
from .device import Device
|
||||
from .kinds import KindMapClass
|
||||
from .kinds import request_kind_map as kindmap
|
||||
from .macrobutton import MacroButton
|
||||
from .recorder import Recorder
|
||||
from .strip import request_strip_obj as strip
|
||||
from .vban import request_vban_obj as vban
|
||||
|
||||
|
||||
class FactoryBuilder:
|
||||
"""
|
||||
Builder class for factories.
|
||||
|
||||
Separates construction from representation.
|
||||
"""
|
||||
|
||||
BuilderProgress = IntEnum(
|
||||
"BuilderProgress", "strip bus command macrobutton vban device recorder", start=0
|
||||
)
|
||||
|
||||
def __init__(self, factory, kind: KindMapClass):
|
||||
self._factory = factory
|
||||
self.kind = kind
|
||||
self._info = (
|
||||
f"Finished building strips for {self._factory}",
|
||||
f"Finished building buses for {self._factory}",
|
||||
f"Finished building commands for {self._factory}",
|
||||
f"Finished building macrobuttons for {self._factory}",
|
||||
f"Finished building vban in/out streams for {self._factory}",
|
||||
f"Finished building device for {self._factory}",
|
||||
f"Finished building recorder for {self._factory}",
|
||||
)
|
||||
|
||||
def _pinfo(self, name: str) -> NoReturn:
|
||||
"""prints progress status for each step"""
|
||||
name = name.split("_")[1]
|
||||
print(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
|
||||
def make_strip(self) -> Self:
|
||||
self._factory.strip = tuple(
|
||||
strip(self.kind.phys_in < i, self._factory, i)
|
||||
for i in range(self.kind.num_strip)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_bus(self) -> Self:
|
||||
self._factory.bus = tuple(
|
||||
bus(self.kind.phys_out < i, self._factory, i)
|
||||
for i in range(self.kind.num_bus)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_command(self) -> Self:
|
||||
self._factory.command = Command.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_macrobutton(self) -> Self:
|
||||
self._factory.button = tuple(MacroButton(self._factory, i) for i in range(80))
|
||||
return self
|
||||
|
||||
def make_vban(self) -> Self:
|
||||
self._factory.vban = vban(self._factory)
|
||||
return self
|
||||
|
||||
def make_device(self) -> Self:
|
||||
self._factory.device = Device.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_recorder(self) -> Self:
|
||||
self._factory.recorder = Recorder.make(self._factory)
|
||||
return self
|
||||
|
||||
|
||||
class FactoryBase(Remote):
|
||||
"""Base class for factories, subclasses Remote."""
|
||||
|
||||
def __init__(self, kind_id: str, **kwargs):
|
||||
defaultkwargs = {"sync": False, "ratelimit": 0.033}
|
||||
kwargs = defaultkwargs | kwargs
|
||||
self.kind = kindmap(kind_id)
|
||||
super().__init__(**kwargs)
|
||||
self.builder = FactoryBuilder(self, self.kind)
|
||||
self._steps = (
|
||||
self.builder.make_strip,
|
||||
self.builder.make_bus,
|
||||
self.builder.make_command,
|
||||
self.builder.make_macrobutton,
|
||||
self.builder.make_vban,
|
||||
self.builder.make_device,
|
||||
)
|
||||
self._configs = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Voicemeeter {self.kind}"
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def steps(self):
|
||||
pass
|
||||
|
||||
@cached_property
|
||||
def configs(self):
|
||||
self._configs = configs(self.kind.name)
|
||||
return self._configs
|
||||
|
||||
|
||||
class BasicFactory(FactoryBase):
|
||||
"""
|
||||
Represents a Basic Remote subclass
|
||||
|
||||
Responsible for directing the builder class
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BasicFactory:
|
||||
raise TypeError(f"'{cls.__name__}' does not support direct instantiation")
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, kind_id, **kwargs):
|
||||
super().__init__(kind_id, **kwargs)
|
||||
[step()._pinfo(step.__name__) for step in self.steps]
|
||||
|
||||
@property
|
||||
def steps(self) -> Iterable:
|
||||
"""steps required to build the interface for a kind"""
|
||||
return self._steps
|
||||
|
||||
|
||||
class BananaFactory(FactoryBase):
|
||||
"""
|
||||
Represents a Banana Remote subclass
|
||||
|
||||
Responsible for directing the builder class
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is BananaFactory:
|
||||
raise TypeError(f"'{cls.__name__}' does not support direct instantiation")
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, kind_id, **kwargs):
|
||||
super().__init__(kind_id, **kwargs)
|
||||
[step()._pinfo(step.__name__) for step in self.steps]
|
||||
|
||||
@property
|
||||
def steps(self) -> Iterable:
|
||||
"""steps required to build the interface for a kind"""
|
||||
return self._steps + (self.builder.make_recorder,)
|
||||
|
||||
|
||||
class PotatoFactory(FactoryBase):
|
||||
"""
|
||||
Represents a Potato Remote subclass
|
||||
|
||||
Responsible for directing the builder class
|
||||
"""
|
||||
|
||||
def __new__(cls, *args, **kwargs):
|
||||
if cls is PotatoFactory:
|
||||
raise TypeError(f"'{cls.__name__}' does not support direct instantiation")
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, kind_id: str, **kwargs):
|
||||
super().__init__(kind_id, **kwargs)
|
||||
[step()._pinfo(step.__name__) for step in self.steps]
|
||||
|
||||
@property
|
||||
def steps(self) -> Iterable:
|
||||
"""steps required to build the interface for a kind"""
|
||||
return self._steps + (self.builder.make_recorder,)
|
||||
|
||||
|
||||
def remote_factory(kind_id: str, **kwargs) -> Remote:
|
||||
"""
|
||||
Factory method, invokes a factory creation class of a kind
|
||||
|
||||
Returns a Remote class of a kind
|
||||
"""
|
||||
match kind_id:
|
||||
case "basic":
|
||||
_factory = BasicFactory
|
||||
case "banana":
|
||||
_factory = BananaFactory
|
||||
case "potato":
|
||||
_factory = PotatoFactory
|
||||
case _:
|
||||
raise ValueError(f"Unknown Voicemeeter kind '{kind_id}'")
|
||||
return type(f"Remote{kind_id.capitalize()}", (_factory,), {})(kind_id, **kwargs)
|
||||
|
||||
|
||||
def request_remote_obj(kind_id: str, **kwargs) -> Remote:
|
||||
"""
|
||||
Interface entry point. Wraps factory method and handles errors
|
||||
|
||||
Returns a reference to a Remote class of a kind
|
||||
"""
|
||||
REMOTE_obj = None
|
||||
try:
|
||||
REMOTE_obj = remote_factory(kind_id, **kwargs)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise SystemExit(e)
|
||||
return REMOTE_obj
|
||||
41
voicemeeterlib/inst.py
Normal file
41
voicemeeterlib/inst.py
Normal file
@@ -0,0 +1,41 @@
|
||||
import ctypes as ct
|
||||
import platform
|
||||
import winreg
|
||||
from pathlib import Path
|
||||
|
||||
from .error import InstallError
|
||||
|
||||
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
|
||||
|
||||
if platform.system() != "Windows":
|
||||
raise InstallError("Only Windows OS supported")
|
||||
|
||||
|
||||
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
||||
REG_KEY = "".join(
|
||||
[
|
||||
"SOFTWARE",
|
||||
("\\WOW6432Node" if bits == 64 else ""),
|
||||
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def get_vmpath():
|
||||
with winreg.OpenKey(
|
||||
winreg.HKEY_LOCAL_MACHINE, r"{}".format(REG_KEY + "\\" + VM_KEY)
|
||||
) as vm_key:
|
||||
path = winreg.QueryValueEx(vm_key, r"UninstallString")[0]
|
||||
return path
|
||||
|
||||
|
||||
vm_path = Path(get_vmpath())
|
||||
vm_parent = vm_path.parent
|
||||
|
||||
DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll'
|
||||
|
||||
dll_path = vm_parent.joinpath(DLL_NAME)
|
||||
if not dll_path.is_file():
|
||||
raise InstallError(f"Could not find {DLL_NAME}")
|
||||
|
||||
libc = ct.CDLL(str(dll_path))
|
||||
36
voicemeeterlib/iremote.py
Normal file
36
voicemeeterlib/iremote.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Self
|
||||
|
||||
|
||||
class IRemote(metaclass=ABCMeta):
|
||||
"""
|
||||
Common interface between base class and extended (higher) classes
|
||||
|
||||
Provides some default implementation
|
||||
"""
|
||||
|
||||
def __init__(self, remote, index=None):
|
||||
self._remote = remote
|
||||
self.index = index
|
||||
|
||||
def getter(self, param, **kwargs):
|
||||
"""Gets a parameter value"""
|
||||
return self._remote.get(f"{self.identifier}.{param}", **kwargs)
|
||||
|
||||
def setter(self, param, val):
|
||||
"""Sets a parameter value"""
|
||||
self._remote.set(f"{self.identifier}.{param}", val)
|
||||
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
def apply(self, data: dict) -> Self:
|
||||
for attr, val in data.items():
|
||||
if hasattr(self, attr):
|
||||
setattr(self, attr, val)
|
||||
return self
|
||||
|
||||
def then_wait(self):
|
||||
time.sleep(self._remote.DELAY)
|
||||
104
voicemeeterlib/kinds.py
Normal file
104
voicemeeterlib/kinds.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
BASIC = 1
|
||||
BANANA = 2
|
||||
POTATO = 3
|
||||
|
||||
|
||||
class SingletonType(type):
|
||||
"""ensure only a single instance of a kind map object"""
|
||||
|
||||
_instances = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
if cls not in cls._instances:
|
||||
cls._instances[cls] = super(SingletonType, cls).__call__(*args, **kwargs)
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
@dataclass
|
||||
class KindMapClass(metaclass=SingletonType):
|
||||
name: str
|
||||
ins: tuple
|
||||
outs: tuple
|
||||
vban: tuple
|
||||
|
||||
@property
|
||||
def phys_in(self):
|
||||
return self.ins[0]
|
||||
|
||||
@property
|
||||
def virt_in(self):
|
||||
return self.ins[-1]
|
||||
|
||||
@property
|
||||
def phys_out(self):
|
||||
return self.outs[0]
|
||||
|
||||
@property
|
||||
def virt_out(self):
|
||||
return self.outs[-1]
|
||||
|
||||
@property
|
||||
def num_strip(self):
|
||||
return sum(self.ins)
|
||||
|
||||
@property
|
||||
def num_bus(self):
|
||||
return sum(self.outs)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.capitalize()
|
||||
|
||||
|
||||
@dataclass
|
||||
class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8)
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
match kind_id:
|
||||
case "basic":
|
||||
_kind_map = BasicMap
|
||||
case "banana":
|
||||
_kind_map = BananaMap
|
||||
case "potato":
|
||||
_kind_map = PotatoMap
|
||||
case _:
|
||||
raise ValueError(f"Unknown Voicemeeter kind {kind_id}")
|
||||
return _kind_map(name=kind_id)
|
||||
|
||||
|
||||
def request_kind_map(kind_id):
|
||||
KIND_obj = None
|
||||
try:
|
||||
KIND_obj = kind_factory(kind_id)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
return KIND_obj
|
||||
|
||||
|
||||
kinds_all = list(request_kind_map(kind_id.name.lower()) for kind_id in KindId)
|
||||
52
voicemeeterlib/macrobutton.py
Normal file
52
voicemeeterlib/macrobutton.py
Normal file
@@ -0,0 +1,52 @@
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
|
||||
|
||||
class Adapter(IRemote):
|
||||
"""Adapter to the common interface."""
|
||||
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
def getter(self, id, mode):
|
||||
return self._remote.get_buttonstatus(id, mode)
|
||||
|
||||
def setter(self, id, val, mode):
|
||||
self._remote.set_buttonstatus(id, val, mode)
|
||||
|
||||
|
||||
class MacroButton(Adapter):
|
||||
"""Defines concrete implementation for macrobutton"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def state(self) -> bool:
|
||||
return self.getter(self.index, 1) == 1
|
||||
|
||||
@state.setter
|
||||
def state(self, val):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("state is a boolean parameter")
|
||||
self.setter(self.index, 1 if val else 0, 1)
|
||||
|
||||
@property
|
||||
def stateonly(self) -> bool:
|
||||
return self.getter(self.index, 2) == 1
|
||||
|
||||
@stateonly.setter
|
||||
def stateonly(self, val):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("stateonly is a boolean parameter")
|
||||
self.setter(self.index, 1 if val else 0, 2)
|
||||
|
||||
@property
|
||||
def trigger(self) -> bool:
|
||||
return self.getter(self.index, 3) == 1
|
||||
|
||||
@trigger.setter
|
||||
def trigger(self, val):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("trigger is a boolean parameter")
|
||||
self.setter(self.index, 1 if val else 0, 3)
|
||||
51
voicemeeterlib/meta.py
Normal file
51
voicemeeterlib/meta.py
Normal file
@@ -0,0 +1,51 @@
|
||||
from .error import VMError
|
||||
|
||||
|
||||
def bool_prop(param):
|
||||
"""meta function for boolean parameters"""
|
||||
|
||||
def fget(self) -> bool:
|
||||
return self.getter(param) == 1
|
||||
|
||||
def fset(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError(f"{param} is a boolean parameter")
|
||||
self.setter(param, 1 if val else 0)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def float_prop(param):
|
||||
"""meta function for float parameters"""
|
||||
|
||||
def fget(self):
|
||||
return self.getter(param)
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def action_prop(param, val: int = 1):
|
||||
"""A param that performs an action"""
|
||||
|
||||
def fdo(self):
|
||||
self.setter(param, val)
|
||||
|
||||
return fdo
|
||||
|
||||
|
||||
def bus_mode_prop(param):
|
||||
"""meta function for bus mode parameters"""
|
||||
|
||||
def fget(self) -> bool:
|
||||
self._remote.clear_dirty()
|
||||
return self.getter(param) == 1
|
||||
|
||||
def fset(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError(f"{param} is a boolean parameter")
|
||||
self.setter(param, 1 if val else 0)
|
||||
|
||||
return property(fget, fset)
|
||||
76
voicemeeterlib/recorder.py
Normal file
76
voicemeeterlib/recorder.py
Normal file
@@ -0,0 +1,76 @@
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
from .meta import action_prop, bool_prop
|
||||
|
||||
|
||||
class Recorder(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for recorder
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def make(cls, remote):
|
||||
"""
|
||||
Factory function for recorder.
|
||||
|
||||
Returns a Recorder class of a kind.
|
||||
"""
|
||||
ChannelMixin = _channel_mixins[remote.kind.name]
|
||||
REC_cls = type(
|
||||
f"Recorder{remote.kind}",
|
||||
(cls, ChannelMixin),
|
||||
{
|
||||
**{
|
||||
param: action_prop(param)
|
||||
for param in [
|
||||
"play",
|
||||
"stop",
|
||||
"pause",
|
||||
"replay",
|
||||
"record",
|
||||
"ff",
|
||||
"rw",
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
return REC_cls(remote)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}"
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return "recorder"
|
||||
|
||||
def load(self, file: str):
|
||||
try:
|
||||
self.setter("load", file)
|
||||
except UnicodeError:
|
||||
raise VMError("File full directory must be a raw string")
|
||||
|
||||
def set_loop(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("Error True or False expected")
|
||||
self.setter("mode.loop", 1 if val else 0)
|
||||
|
||||
loop = property(fset=set_loop)
|
||||
|
||||
|
||||
def _make_channel_mixin(kind):
|
||||
"""Creates a channel out property mixin"""
|
||||
num_A, num_B = kind.outs
|
||||
return type(
|
||||
f"ChannelMixin{kind.name}",
|
||||
(),
|
||||
{
|
||||
**{f"A{i}": bool_prop(f"A{i}") for i in range(1, num_A + 1)},
|
||||
**{f"B{i}": bool_prop(f"B{i}") for i in range(1, num_B + 1)},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
_channel_mixins = {kind.name: _make_channel_mixin(kind) for kind in kinds_all}
|
||||
320
voicemeeterlib/strip.py
Normal file
320
voicemeeterlib/strip.py
Normal file
@@ -0,0 +1,320 @@
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from math import log
|
||||
from typing import Union
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
from .meta import bool_prop
|
||||
|
||||
|
||||
class Strip(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for strip
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def mono(self) -> bool:
|
||||
return self.getter("mono") == 1
|
||||
|
||||
@mono.setter
|
||||
def mono(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("mono is a boolean parameter")
|
||||
self.setter("mono", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def solo(self) -> bool:
|
||||
return self.getter("solo") == 1
|
||||
|
||||
@solo.setter
|
||||
def solo(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("solo is a boolean parameter")
|
||||
self.setter("solo", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def mute(self) -> bool:
|
||||
return self.getter("mute") == 1
|
||||
|
||||
@mute.setter
|
||||
def mute(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("mute is a boolean parameter")
|
||||
self.setter("mute", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
return int(self.getter("limit"))
|
||||
|
||||
@limit.setter
|
||||
def limit(self, val: int):
|
||||
self.setter("limit", val)
|
||||
|
||||
@property
|
||||
def label(self) -> str:
|
||||
return self.getter("Label", is_string=True)
|
||||
|
||||
@label.setter
|
||||
def label(self, val: str):
|
||||
if not isinstance(val, str):
|
||||
raise VMError("label is a string parameter")
|
||||
self.setter("Label", val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return round(self.getter("gain"), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self.remote.delay)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self.remote.delay)
|
||||
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@property
|
||||
def comp(self) -> float:
|
||||
return round(self.getter("Comp"), 1)
|
||||
|
||||
@comp.setter
|
||||
def comp(self, val: float):
|
||||
self.setter("Comp", val)
|
||||
|
||||
@property
|
||||
def gate(self) -> float:
|
||||
return round(self.getter("Gate"), 1)
|
||||
|
||||
@gate.setter
|
||||
def gate(self, val: float):
|
||||
self.setter("Gate", val)
|
||||
|
||||
@property
|
||||
def audibility(self) -> float:
|
||||
return round(self.getter("audibility"), 1)
|
||||
|
||||
@audibility.setter
|
||||
def audibility(self, val: float):
|
||||
self.setter("audibility", val)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self.getter("device.name", is_string=True)
|
||||
|
||||
@property
|
||||
def sr(self):
|
||||
return int(self.getter("device.sr"))
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@property
|
||||
def mc(self) -> bool:
|
||||
return self.getter("mc") == 1
|
||||
|
||||
@mc.setter
|
||||
def mc(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("mc is a boolean parameter")
|
||||
self.setter("mc", 1 if val else 0)
|
||||
|
||||
mono = mc
|
||||
|
||||
@property
|
||||
def k(self) -> int:
|
||||
return int(self.getter("karaoke"))
|
||||
|
||||
@k.setter
|
||||
def k(self, val: int):
|
||||
self.setter("karaoke", val)
|
||||
|
||||
@property
|
||||
def bass(self):
|
||||
return round(self.getter("EQGain1"), 1)
|
||||
|
||||
@bass.setter
|
||||
def bass(self, val: float):
|
||||
self.setter("EQGain1", val)
|
||||
|
||||
@property
|
||||
def mid(self):
|
||||
return round(self.getter("EQGain2"), 1)
|
||||
|
||||
@mid.setter
|
||||
def mid(self, val: float):
|
||||
self.setter("EQGain2", val)
|
||||
|
||||
med = mid
|
||||
|
||||
@property
|
||||
def treble(self):
|
||||
return round(self.getter("EQGain3"), 1)
|
||||
|
||||
@treble.setter
|
||||
def treble(self, val: float):
|
||||
self.setter("EQGain3", val)
|
||||
|
||||
def appgain(self, name: str, gain: float):
|
||||
self.setter("AppGain", f'("{name}", {gain})')
|
||||
|
||||
def appmute(self, name: str, mute: bool = None):
|
||||
if not isinstance(mute, bool) and mute not in (0, 1):
|
||||
raise VMError("appmute is a boolean parameter")
|
||||
self.setter("AppMute", f'("{name}", {1 if mute else 0})')
|
||||
|
||||
|
||||
class StripLevel(IRemote):
|
||||
def __init__(self, remote, index):
|
||||
super().__init__(remote, index)
|
||||
phys_map = tuple((i, i + 2) for i in range(0, remote.kind.phys_in * 2, 2))
|
||||
virt_map = tuple(
|
||||
(i, i + 8)
|
||||
for i in range(
|
||||
remote.kind.phys_in * 2,
|
||||
remote.kind.phys_in * 2 + remote.kind.virt_in * 8,
|
||||
8,
|
||||
)
|
||||
)
|
||||
self.level_map = phys_map + virt_map
|
||||
|
||||
def getter(self, mode):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
res = self._remote.get_level(mode, i)
|
||||
return round(20 * log(res, 10), 1) if res > 0 else -200.0
|
||||
|
||||
range_ = self.level_map[self.index]
|
||||
return tuple(fget(i) for i in range(*range_))
|
||||
|
||||
def getter_prefader(self):
|
||||
def fget(i):
|
||||
return round(20 * log(i, 10), 1) if i > 0 else -200.0
|
||||
|
||||
range_ = self.level_map[self.index]
|
||||
return tuple(
|
||||
fget(i) for i in self._remote._strip_levels[range_[0] : range_[-1]]
|
||||
)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def prefader(self) -> tuple:
|
||||
return self.getter_prefader()
|
||||
|
||||
@property
|
||||
def postfader(self) -> tuple:
|
||||
return self.getter(1)
|
||||
|
||||
@property
|
||||
def postmute(self) -> tuple:
|
||||
return self.getter(2)
|
||||
|
||||
@property
|
||||
def updated(self) -> tuple:
|
||||
return self._remote._strip_comp
|
||||
|
||||
|
||||
class GainLayer(IRemote):
|
||||
def __init__(self, remote, index, i):
|
||||
super().__init__(remote, index)
|
||||
self._i = i
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def gain(self):
|
||||
return self.getter(f"GainLayer[{self._i}]")
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val):
|
||||
self.setter(f"GainLayer[{self._i}]", val)
|
||||
|
||||
|
||||
def _make_gainlayer_mixin(remote, index):
|
||||
"""Creates a GainLayer mixin"""
|
||||
return type(
|
||||
f"GainlayerMixin",
|
||||
(),
|
||||
{
|
||||
"gainlayer": tuple(
|
||||
GainLayer(remote, index, i) for i in range(remote.kind.num_bus)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _make_channelout_mixin(kind):
|
||||
"""Creates a channel out property mixin"""
|
||||
return type(
|
||||
f"ChannelOutMixin{kind}",
|
||||
(),
|
||||
{
|
||||
**{f"A{i}": bool_prop(f"A{i}") for i in range(1, kind.phys_out + 1)},
|
||||
**{f"B{i}": bool_prop(f"B{i}") for i in range(1, kind.virt_out + 1)},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
__make_channelout_mixins = {
|
||||
kind.name: _make_channelout_mixin(kind) for kind in kinds_all
|
||||
}
|
||||
|
||||
|
||||
def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip]:
|
||||
"""
|
||||
Factory method for strips
|
||||
|
||||
Mixes in required classes
|
||||
|
||||
Returns a physical or virtual strip subclass
|
||||
"""
|
||||
STRIP_cls = PhysicalStrip if is_phys_strip else VirtualStrip
|
||||
CHANNELOUTMIXIN_cls = __make_channelout_mixins[remote.kind.name]
|
||||
|
||||
_kls = (STRIP_cls, CHANNELOUTMIXIN_cls)
|
||||
if remote.kind.name == "potato":
|
||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||
_kls += (GAINLAYERMIXIN_cls,)
|
||||
return type(
|
||||
f"{STRIP_cls.__name__}{remote.kind}",
|
||||
_kls,
|
||||
{
|
||||
"levels": StripLevel(remote, i),
|
||||
},
|
||||
)(remote, i)
|
||||
|
||||
|
||||
def request_strip_obj(is_phys_strip, remote, i) -> Strip:
|
||||
"""
|
||||
Strip entry point. Wraps factory method.
|
||||
|
||||
Returns a reference to a strip subclass of a kind
|
||||
"""
|
||||
return strip_factory(is_phys_strip, remote, i)
|
||||
39
voicemeeterlib/subject.py
Normal file
39
voicemeeterlib/subject.py
Normal file
@@ -0,0 +1,39 @@
|
||||
class Subject:
|
||||
"""Adds support for observers"""
|
||||
|
||||
def __init__(self):
|
||||
"""list of current observers"""
|
||||
|
||||
self._observers = list()
|
||||
|
||||
@property
|
||||
def observers(self) -> list:
|
||||
"""returns the current observers"""
|
||||
|
||||
return self._observers
|
||||
|
||||
def notify(self, modifier=None, data=None):
|
||||
"""run callbacks on update"""
|
||||
|
||||
[o.on_update(modifier, data) for o in self._observers]
|
||||
|
||||
def add(self, observer):
|
||||
"""adds an observer to _observers"""
|
||||
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
else:
|
||||
print(f"Failed to add: {observer}")
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from _observers"""
|
||||
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
except ValueError:
|
||||
print(f"Failed to remove: {observer}")
|
||||
|
||||
def clear(self):
|
||||
"""clears the _observers list"""
|
||||
|
||||
self._observers.clear()
|
||||
52
voicemeeterlib/util.py
Normal file
52
voicemeeterlib/util.py
Normal file
@@ -0,0 +1,52 @@
|
||||
import functools
|
||||
|
||||
|
||||
def polling(func):
|
||||
"""
|
||||
Offers memoization for a set into get operation.
|
||||
|
||||
If sync clear dirty parameters before fetching new value.
|
||||
|
||||
Useful for loop getting if not running callbacks
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
get = func.__name__ == "get"
|
||||
mb_get = func.__name__ == "get_buttonstatus"
|
||||
remote, *remaining = args
|
||||
|
||||
if get:
|
||||
param, *rem = remaining
|
||||
elif mb_get:
|
||||
id, mode, *rem = remaining
|
||||
param = f"mb_{id}_{mode}"
|
||||
|
||||
if param in remote.cache:
|
||||
return remote.cache.pop(param)
|
||||
if remote.sync:
|
||||
remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def script(func):
|
||||
"""Convert dictionary to script"""
|
||||
|
||||
def wrapper(*args):
|
||||
remote, script = args
|
||||
if isinstance(script, dict):
|
||||
params = ""
|
||||
for key, val in script.items():
|
||||
obj, m2, *rem = key.split("-")
|
||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
||||
params += ";".join(
|
||||
f"{obj}{f'.{m2}stream' if not m2.isnumeric() else ''}[{index}].{k}={int(v) if isinstance(v, bool) else v}"
|
||||
for k, v in val.items()
|
||||
)
|
||||
params += ";"
|
||||
script = params
|
||||
return func(remote, script)
|
||||
|
||||
return wrapper
|
||||
188
voicemeeterlib/vban.py
Normal file
188
voicemeeterlib/vban.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for vban stream
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"vban.{self.direction}stream[{self.index}]"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter("on") == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
if not isinstance(val, bool) and val not in (0, 1):
|
||||
raise VMError("True or False expected")
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.getter("name", is_string=True)
|
||||
|
||||
@name.setter
|
||||
def name(self, val: str):
|
||||
self.setter("name", val)
|
||||
|
||||
@property
|
||||
def ip(self) -> str:
|
||||
return self.getter("ip", is_string=True)
|
||||
|
||||
@ip.setter
|
||||
def ip(self, val: str):
|
||||
self.setter("ip", val)
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
return int(self.getter("port"))
|
||||
|
||||
@port.setter
|
||||
def port(self, val: int):
|
||||
if val not in range(1024, 65536):
|
||||
raise VMError("Expected value from 1024 to 65535")
|
||||
self.setter("port", val)
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return int(self.getter("sr"))
|
||||
|
||||
@sr.setter
|
||||
def sr(self, val: int):
|
||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||
if val not in opts:
|
||||
raise VMError("Expected one of: {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
def channel(self) -> int:
|
||||
return int(self.getter("channel"))
|
||||
|
||||
@channel.setter
|
||||
def channel(self, val: int):
|
||||
if val not in range(1, 9):
|
||||
raise VMError("Expected value from 1 to 8")
|
||||
self.setter("channel", val)
|
||||
|
||||
@property
|
||||
def bit(self) -> int:
|
||||
return 16 if (int(self.getter("bit") == 1)) else 24
|
||||
|
||||
@bit.setter
|
||||
def bit(self, val: int):
|
||||
if val not in (16, 24):
|
||||
raise VMError("Expected value 16 or 24")
|
||||
self.setter("bit", 1 if (val == 16) else 2)
|
||||
|
||||
@property
|
||||
def quality(self) -> int:
|
||||
return int(self.getter("quality"))
|
||||
|
||||
@quality.setter
|
||||
def quality(self, val: int):
|
||||
if val not in range(5):
|
||||
raise VMError("Expected value from 0 to 4")
|
||||
self.setter("quality", val)
|
||||
|
||||
@property
|
||||
def route(self) -> int:
|
||||
return int(self.getter("route"))
|
||||
|
||||
@route.setter
|
||||
def route(self, val: int):
|
||||
if val not in range(9):
|
||||
raise VMError("Expected value from 0 to 8")
|
||||
self.setter("route", val)
|
||||
|
||||
|
||||
class VbanInstream(VbanStream):
|
||||
"""
|
||||
class representing a vban instream
|
||||
|
||||
subclasses VbanStream
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return "in"
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return super(VbanInstream, self).sr
|
||||
|
||||
@property
|
||||
def channel(self) -> int:
|
||||
return super(VbanInstream, self).channel
|
||||
|
||||
@property
|
||||
def bit(self) -> int:
|
||||
return super(VbanInstream, self).bit
|
||||
|
||||
|
||||
class VbanOutstream(VbanStream):
|
||||
"""
|
||||
class representing a vban outstream
|
||||
|
||||
Subclasses VbanStream
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return "out"
|
||||
|
||||
|
||||
class Vban:
|
||||
"""
|
||||
class representing the vban module
|
||||
|
||||
Contains two tuples, one for each stream type
|
||||
"""
|
||||
|
||||
def __init__(self, remote):
|
||||
self.remote = remote
|
||||
num_instream, num_outstream = remote.kind.vban
|
||||
self.instream = tuple(VbanInstream(remote, i) for i in range(num_instream))
|
||||
self.outstream = tuple(VbanOutstream(remote, i) for i in range(num_outstream))
|
||||
|
||||
def enable(self):
|
||||
self.remote.set("vban.Enable", 1)
|
||||
|
||||
def disable(self):
|
||||
self.remote.set("vban.Enable", 0)
|
||||
|
||||
|
||||
def vban_factory(remote) -> Vban:
|
||||
"""
|
||||
Factory method for vban
|
||||
|
||||
Returns a class that represents the VBAN module.
|
||||
"""
|
||||
VBAN_cls = Vban
|
||||
return type(f"{VBAN_cls.__name__}", (VBAN_cls,), {})(remote)
|
||||
|
||||
|
||||
def request_vban_obj(remote) -> Vban:
|
||||
"""
|
||||
Vban entry point.
|
||||
|
||||
Returns a reference to a Vban class of a kind
|
||||
"""
|
||||
return vban_factory(remote)
|
||||
Reference in New Issue
Block a user