11 Commits

Author SHA1 Message Date
2d5c611ed8 add button color example to readme 2023-07-23 08:33:54 +01:00
46e7ffe478 adds MacroButtonColorMixin. 2023-07-23 07:56:09 +01:00
bf77ded007 fix bind name for get_num_devices.
patch bump
2023-07-21 12:56:12 +01:00
236125d095 patch bump 2023-07-21 12:51:01 +01:00
7841dfe10f add ButtonModes IntEnum to macrobutton
debug log getter,setter for macrobutton
2023-07-21 12:49:01 +01:00
bdf8dc489a rename bindings to match CAPI functions
use prefix bind_
2023-07-21 12:47:53 +01:00
160a6f89f9 patch bump 2023-07-20 11:12:34 +01:00
4fcb2f93ca remove unused import 2023-07-20 11:11:44 +01:00
8acd0b1385 add missing type annotations 2023-07-20 11:11:34 +01:00
89866bb87b remove redundant __str__ overrides 2023-07-20 11:10:37 +01:00
f996fc0d9c num_strip_levels, num_bus_levesl added to KindMaps 2023-07-20 11:10:05 +01:00
12 changed files with 168 additions and 137 deletions

View File

@@ -380,12 +380,13 @@ The following properties are available.
- `state`: boolean
- `stateonly`: boolean
- `trigger`: boolean
- `color`: int, from 0 to 8
example:
```python
vm.button[37].state = True
vm.button[55].trigger = False
vm.button[4].color = 1
```
### Recorder

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "voicemeeter-api"
version = "2.3.3"
version = "2.3.6"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"

View File

@@ -18,98 +18,98 @@ class CBindings(metaclass=ABCMeta):
logger_cbindings = logger.getChild("CBindings")
vm_login = libc.VBVMR_Login
vm_login.restype = LONG
vm_login.argtypes = None
bind_login = libc.VBVMR_Login
bind_login.restype = LONG
bind_login.argtypes = None
vm_logout = libc.VBVMR_Logout
vm_logout.restype = LONG
vm_logout.argtypes = None
bind_logout = libc.VBVMR_Logout
bind_logout.restype = LONG
bind_logout.argtypes = None
vm_runvm = libc.VBVMR_RunVoicemeeter
vm_runvm.restype = LONG
vm_runvm.argtypes = [LONG]
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter
bind_run_voicemeeter.restype = LONG
bind_run_voicemeeter.argtypes = [LONG]
vm_get_type = libc.VBVMR_GetVoicemeeterType
vm_get_type.restype = LONG
vm_get_type.argtypes = [ct.POINTER(LONG)]
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType
bind_get_voicemeeter_type.restype = LONG
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
vm_get_version = libc.VBVMR_GetVoicemeeterVersion
vm_get_version.restype = LONG
vm_get_version.argtypes = [ct.POINTER(LONG)]
bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
bind_get_voicemeeter_version.restype = LONG
bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
if hasattr(libc, "VBVMR_MacroButton_IsDirty"):
vm_mdirty = libc.VBVMR_MacroButton_IsDirty
vm_mdirty.restype = LONG
vm_mdirty.argtypes = None
bind_macro_button_is_dirty = libc.VBVMR_MacroButton_IsDirty
bind_macro_button_is_dirty.restype = LONG
bind_macro_button_is_dirty.argtypes = None
if hasattr(libc, "VBVMR_MacroButton_GetStatus"):
vm_get_buttonstatus = libc.VBVMR_MacroButton_GetStatus
vm_get_buttonstatus.restype = LONG
vm_get_buttonstatus.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
bind_macro_button_get_status = libc.VBVMR_MacroButton_GetStatus
bind_macro_button_get_status.restype = LONG
bind_macro_button_get_status.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
if hasattr(libc, "VBVMR_MacroButton_SetStatus"):
vm_set_buttonstatus = libc.VBVMR_MacroButton_SetStatus
vm_set_buttonstatus.restype = LONG
vm_set_buttonstatus.argtypes = [LONG, FLOAT, LONG]
bind_macro_button_set_status = libc.VBVMR_MacroButton_SetStatus
bind_macro_button_set_status.restype = LONG
bind_macro_button_set_status.argtypes = [LONG, FLOAT, LONG]
vm_pdirty = libc.VBVMR_IsParametersDirty
vm_pdirty.restype = LONG
vm_pdirty.argtypes = None
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
bind_is_parameters_dirty.restype = LONG
bind_is_parameters_dirty.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)]
bind_get_parameter_float = libc.VBVMR_GetParameterFloat
bind_get_parameter_float.restype = LONG
bind_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]
bind_set_parameter_float = libc.VBVMR_SetParameterFloat
bind_set_parameter_float.restype = LONG
bind_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)]
bind_get_parameter_string_w = libc.VBVMR_GetParameterStringW
bind_get_parameter_string_w.restype = LONG
bind_get_parameter_string_w.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)]
bind_set_parameter_string_w = libc.VBVMR_SetParameterStringW
bind_set_parameter_string_w.restype = LONG
bind_set_parameter_string_w.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)]
bind_set_parameters = libc.VBVMR_SetParameters
bind_set_parameters.restype = LONG
bind_set_parameters.argtypes = [ct.POINTER(CHAR)]
vm_get_level = libc.VBVMR_GetLevel
vm_get_level.restype = LONG
vm_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
bind_get_level = libc.VBVMR_GetLevel
bind_get_level.restype = LONG
bind_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
bind_input_get_device_number = libc.VBVMR_Input_GetDeviceNumber
bind_input_get_device_number.restype = LONG
bind_input_get_device_number.argtypes = None
vm_get_desc_indevices = libc.VBVMR_Input_GetDeviceDescW
vm_get_desc_indevices.restype = LONG
vm_get_desc_indevices.argtypes = [
bind_input_get_device_desc_w = libc.VBVMR_Input_GetDeviceDescW
bind_input_get_device_desc_w.restype = LONG
bind_input_get_device_desc_w.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
bind_output_get_device_number = libc.VBVMR_Output_GetDeviceNumber
bind_output_get_device_number.restype = LONG
bind_output_get_device_number.argtypes = None
vm_get_desc_outdevices = libc.VBVMR_Output_GetDeviceDescW
vm_get_desc_outdevices.restype = LONG
vm_get_desc_outdevices.argtypes = [
bind_output_get_device_desc_w = libc.VBVMR_Output_GetDeviceDescW
bind_output_get_device_desc_w.restype = LONG
bind_output_get_device_desc_w.argtypes = [
LONG,
ct.POINTER(LONG),
ct.POINTER(WCHAR * 256),
ct.POINTER(WCHAR * 256),
]
vm_get_midi_message = libc.VBVMR_GetMidiMessage
vm_get_midi_message.restype = LONG
vm_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
bind_get_midi_message = libc.VBVMR_GetMidiMessage
bind_get_midi_message.restype = LONG
bind_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
def call(self, func, *args, ok=(0,), ok_exp=None):
try:

View File

@@ -23,6 +23,7 @@ class Adapter(IRemote):
def output(self):
pass
@property
def identifier(self):
pass

View File

@@ -33,6 +33,7 @@ class IRemote(metaclass=ABCMeta):
cmd += (f".{param}",)
return "".join(cmd)
@property
@abstractmethod
def identifier(self):
pass

View File

@@ -32,29 +32,37 @@ class KindMapClass(metaclass=SingletonType):
insert: int
@property
def phys_in(self):
def phys_in(self) -> int:
return self.ins[0]
@property
def virt_in(self):
def virt_in(self) -> int:
return self.ins[-1]
@property
def phys_out(self):
def phys_out(self) -> int:
return self.outs[0]
@property
def virt_out(self):
def virt_out(self) -> int:
return self.outs[-1]
@property
def num_strip(self):
def num_strip(self) -> int:
return sum(self.ins)
@property
def num_bus(self):
def num_bus(self) -> int:
return sum(self.outs)
@property
def num_strip_levels(self) -> int:
return 2 * self.phys_in + 8 * self.virt_in
@property
def num_bus_levels(self) -> int:
return 8 * (self.phys_out + self.virt_out)
def __str__(self) -> str:
return self.name.capitalize()

View File

@@ -1,20 +1,43 @@
from enum import IntEnum
from .iremote import IRemote
ButtonModes = IntEnum(
"ButtonModes",
"state stateonly trigger",
start=1,
)
class Adapter(IRemote):
"""Adapter to the common interface."""
def identifier(self):
pass
def getter(self, mode):
self.logger.debug(f"getter: button[{self.index}].{ButtonModes(mode).name}")
return self._remote.get_buttonstatus(self.index, mode)
def setter(self, val, mode):
def setter(self, mode, val):
self.logger.debug(
f"setter: button[{self.index}].{ButtonModes(mode).name}={val}"
)
self._remote.set_buttonstatus(self.index, val, mode)
class MacroButton(Adapter):
class MacroButtonColorMixin(IRemote):
@property
def identifier(self):
return f"command.button[{self.index}]"
@property
def color(self) -> int:
return int(IRemote.getter(self, "color"))
@color.setter
def color(self, val: int):
IRemote.setter(self, "color", val)
class MacroButton(Adapter, MacroButtonColorMixin):
"""Defines concrete implementation for macrobutton"""
def __str__(self):
@@ -22,24 +45,24 @@ class MacroButton(Adapter):
@property
def state(self) -> bool:
return self.getter(1) == 1
return self.getter(ButtonModes.state) == 1
@state.setter
def state(self, val):
self.setter(1 if val else 0, 1)
def state(self, val: bool):
self.setter(ButtonModes.state, 1 if val else 0)
@property
def stateonly(self) -> bool:
return self.getter(2) == 1
return self.getter(ButtonModes.stateonly) == 1
@stateonly.setter
def stateonly(self, val):
self.setter(1 if val else 0, 2)
def stateonly(self, val: bool):
self.setter(ButtonModes.stateonly, 1 if val else 0)
@property
def trigger(self) -> bool:
return self.getter(3) == 1
return self.getter(ButtonModes.trigger) == 1
@trigger.setter
def trigger(self, val):
self.setter(1 if val else 0, 3)
def trigger(self, val: bool):
self.setter(ButtonModes.trigger, 1 if val else 0)

View File

@@ -1,4 +1,3 @@
import copy
import ctypes as ct
import logging
import time
@@ -65,7 +64,7 @@ class Remote(CBindings):
def login(self) -> NoReturn:
"""Login to the API, initialize dirty parameters"""
self.gui.launched = self.call(self.vm_login, ok=(0, 1)) == 0
self.gui.launched = self.call(self.bind_login, ok=(0, 1)) == 0
if not self.gui.launched:
self.logger.info(
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
@@ -83,21 +82,21 @@ class Remote(CBindings):
value = KindId[kind_id.upper()].value + 3
else:
value = KindId[kind_id.upper()].value
self.call(self.vm_runvm, value)
self.call(self.bind_run_voicemeeter, value)
time.sleep(1)
@property
def type(self) -> str:
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
type_ = ct.c_long()
self.call(self.vm_get_type, ct.byref(type_))
self.call(self.bind_get_voicemeeter_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.call(self.vm_get_version, ct.byref(ver))
self.call(self.bind_get_voicemeeter_version, ct.byref(ver))
return "{}.{}.{}.{}".format(
(ver.value & 0xFF000000) >> 24,
(ver.value & 0x00FF0000) >> 16,
@@ -108,13 +107,13 @@ class Remote(CBindings):
@property
def pdirty(self) -> bool:
"""True iff UI parameters have been updated."""
return self.call(self.vm_pdirty, ok=(0, 1)) == 1
return self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1
@property
def mdirty(self) -> bool:
"""True iff MB parameters have been updated."""
try:
return self.call(self.vm_mdirty, ok=(0, 1)) == 1
return self.call(self.bind_macro_button_is_dirty, ok=(0, 1)) == 1
except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
ERR_MSG = (
@@ -150,10 +149,10 @@ class Remote(CBindings):
"""Gets a string or float parameter"""
if is_string:
buf = ct.create_unicode_buffer(512)
self.call(self.vm_get_parameter_string, param.encode(), ct.byref(buf))
self.call(self.bind_get_parameter_string_w, param.encode(), ct.byref(buf))
else:
buf = ct.c_float()
self.call(self.vm_get_parameter_float, param.encode(), ct.byref(buf))
self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf))
return buf.value
def set(self, param: str, val: Union[str, float]) -> NoReturn:
@@ -161,22 +160,24 @@ class Remote(CBindings):
if isinstance(val, str):
if len(val) >= 512:
raise VMError("String is too long")
self.call(self.vm_set_parameter_string, param.encode(), ct.c_wchar_p(val))
self.call(
self.bind_set_parameter_string_w, param.encode(), ct.c_wchar_p(val)
)
else:
self.call(
self.vm_set_parameter_float, param.encode(), ct.c_float(float(val))
self.bind_set_parameter_float, param.encode(), ct.c_float(float(val))
)
self.cache[param] = val
@polling
def get_buttonstatus(self, id: int, mode: int) -> int:
def get_buttonstatus(self, id_: int, mode: int) -> int:
"""Gets a macrobutton parameter"""
state = ct.c_float()
c_state = ct.c_float()
try:
self.call(
self.vm_get_buttonstatus,
ct.c_long(id),
ct.byref(state),
self.bind_macro_button_get_status,
ct.c_long(id_),
ct.byref(c_state),
ct.c_long(mode),
)
except AttributeError as e:
@@ -188,13 +189,18 @@ class Remote(CBindings):
raise CAPIError(
"VBVMR_MacroButton_GetStatus", -9, msg=" ".join(ERR_MSG)
) from e
return int(state.value)
return int(c_state.value)
def set_buttonstatus(self, id: int, state: int, mode: int) -> NoReturn:
def set_buttonstatus(self, id_: int, val: int, mode: int) -> NoReturn:
"""Sets a macrobutton parameter. Caches value"""
c_state = ct.c_float(float(state))
c_state = ct.c_float(float(val))
try:
self.call(self.vm_set_buttonstatus, ct.c_long(id), c_state, ct.c_long(mode))
self.call(
self.bind_macro_button_set_status,
ct.c_long(id_),
c_state,
ct.c_long(mode),
)
except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
ERR_MSG = (
@@ -204,13 +210,13 @@ class Remote(CBindings):
raise CAPIError(
"VBVMR_MacroButton_SetStatus", -9, msg=" ".join(ERR_MSG)
) from e
self.cache[f"mb_{id}_{mode}"] = int(c_state.value)
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")
func = getattr(self, f"bind_{direction}put_get_device_number")
res = self.call(func, ok_exp=lambda r: r >= 0)
return res
@@ -221,7 +227,7 @@ class Remote(CBindings):
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 = getattr(self, f"bind_{direction}put_get_device_desc_w")
self.call(
func,
ct.c_long(index),
@@ -234,7 +240,9 @@ class Remote(CBindings):
def get_level(self, type_: int, index: int) -> float:
"""Retrieves a single level value"""
val = ct.c_float()
self.call(self.vm_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val))
self.call(
self.bind_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val)
)
return val.value
def _get_levels(self) -> Iterable:
@@ -244,19 +252,16 @@ class Remote(CBindings):
return (
tuple(
self.get_level(self.strip_mode, 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))
for i in range(self.kind.num_strip_levels)
),
tuple(self.get_level(3, i) for i in range(self.kind.num_bus_levels)),
)
def get_midi_message(self):
n = ct.c_long(1024)
buf = ct.create_string_buffer(1024)
res = self.call(
self.vm_get_midi_message,
self.bind_get_midi_message,
ct.byref(buf),
n,
ok=(-5, -6), # no data received from midi device
@@ -279,7 +284,7 @@ class Remote(CBindings):
"""Sets many parameters from a script"""
if len(script) > 48000:
raise ValueError("Script too large, max size 48kB")
self.call(self.vm_set_parameter_multi, script.encode())
self.call(self.bind_set_parameters, script.encode())
time.sleep(self.DELAY * 5)
def apply(self, data: dict):
@@ -325,16 +330,16 @@ class Remote(CBindings):
self.apply(config)
self.logger.info(f"Profile '{name}' applied!")
def logout(self) -> NoReturn:
"""Wait for dirty parameters to clear, then logout of the API"""
time.sleep(0.1)
self.call(self.vm_logout)
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
def end_thread(self):
self.logger.debug("events thread shutdown started")
self.running = False
def logout(self) -> NoReturn:
"""Logout of the API"""
time.sleep(0.1)
self.call(self.bind_logout)
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
def __exit__(self, exc_type, exc_value, exc_traceback) -> NoReturn:
"""teardown procedures"""
self.end_thread()

View File

@@ -364,7 +364,7 @@ class VirtualStrip(Strip):
self.setter("karaoke", val)
@property
def bass(self):
def bass(self) -> float:
return round(self.getter("EQGain1"), 1)
@bass.setter
@@ -372,7 +372,7 @@ class VirtualStrip(Strip):
self.setter("EQGain1", val)
@property
def mid(self):
def mid(self) -> float:
return round(self.getter("EQGain2"), 1)
@mid.setter
@@ -382,7 +382,7 @@ class VirtualStrip(Strip):
med = mid
@property
def treble(self):
def treble(self) -> float:
return round(self.getter("EQGain3"), 1)
high = treble

View File

@@ -36,10 +36,8 @@ class Updater(threading.Thread):
super().__init__(name="updater", daemon=True)
self._remote = remote
self.queue = queue
self._remote._strip_comp = [False] * (
2 * self._remote.kind.phys_in + 8 * self._remote.kind.virt_in
)
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
(
self._remote.cache["strip_level"],
self._remote.cache["bus_level"],

View File

@@ -1,4 +1,3 @@
import copy
import functools
from itertools import zip_longest
from typing import Iterator

View File

@@ -135,18 +135,15 @@ class VbanInstream(VbanStream):
class VbanAudioInstream(VbanInstream):
def __str__(self):
return f"{type(self).__name__}{self._remote.kind}{self.index}"
"""Represents a VBAN Audio Instream"""
class VbanMidiInstream(VbanInstream):
def __str__(self):
return f"{type(self).__name__}{self._remote.kind}{self.index}"
"""Represents a VBAN Midi Instream"""
class VbanTextInstream(VbanInstream):
def __str__(self):
return f"{type(self).__name__}{self._remote.kind}{self.index}"
"""Represents a VBAN Text Instream"""
class VbanOutstream(VbanStream):
@@ -165,13 +162,11 @@ class VbanOutstream(VbanStream):
class VbanAudioOutstream(VbanOutstream):
def __str__(self):
return f"{type(self).__name__}{self._remote.kind}{self.index}"
"""Represents a VBAN Audio Outstream"""
class VbanMidiOutstream(VbanOutstream):
def __str__(self):
return f"{type(self).__name__}{self._remote.kind}{self.index}"
"""Represents a VBAN Midi Outstream"""
def _make_stream_pair(remote, kind):