2 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
27 changed files with 152 additions and 239 deletions

View File

@@ -11,24 +11,6 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x]
## [2.4.8] - 2023-08-13
### Added
- Error tests added in tests/test_errors.py
- fn_name and code set as class attributes for CAPIError
- Errors section in README updated.
### Changed
- InstallError and CAPIError classes now subclass VMError
## [2.3.7] - 2023-08-01
### Changed
- If the configs loader is passed an invalid config TOML it will log an error but continue to load further configs into memory.
## [2.3.2] - 2023-07-12
### Added

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
@@ -845,10 +846,7 @@ True iff a level has been updated.
- `errors.VMError`: Exception raised when general errors occur.
- `errors.InstallError`: Exception raised when installation errors occur.
- `errors.CAPIError`: Exception raised when the C-API returns error values.
- The following attributes are available:
- `fn_name`: C-API function name.
- `code`: error code
- For a full list of error codes check the [VoicemeeterRemote header file][Voicemeeter Remote Header].
- Error codes are stored in {Exception Class}.code. For a full list of error codes [check the VoicemeeterRemote header file][Voicemeeter Remote Header].
### Logging

View File

@@ -1,5 +1,7 @@
import json
import logging
import time
from logging import config
import voicemeeterlib

View File

@@ -14,20 +14,19 @@ class App:
self.vm.observer.add(self.on_midi)
def on_midi(self):
if self.get_info() == self.MIDI_BUTTON:
self.on_midi_press()
self.get_info()
self.on_midi_press()
def get_info(self):
current = self.vm.midi.current
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
return current
def on_midi_press(self):
"""if midi button 48 is pressed and strip 3 level max > -40, then set trigger for macrobutton 0"""
"""if strip 3 level max > -40 and midi button 48 is pressed, then set trigger for macrobutton 0"""
if (
self.vm.midi.get(self.MIDI_BUTTON) == 127
and max(self.vm.strip[3].levels.postfader) > -40
max(self.vm.strip[3].levels.postfader) > -40
and self.vm.midi.get(self.MIDI_BUTTON) == 127
):
print(
f"Strip 3 level max is greater than -40 and midi button {self.MIDI_BUTTON} is pressed"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "voicemeeter-api"
version = "2.4.8"
version = "2.3.6"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
@@ -33,10 +33,7 @@ levels = "scripts:ex_levels"
midi = "scripts:ex_midi"
obs = "scripts:ex_obs"
observer = "scripts:ex_observer"
basic = "scripts:test_basic"
banana = "scripts:test_banana"
potato = "scripts:test_potato"
all = "scripts:test_all"
test = "scripts:test"
[tool.tox]
legacy_tox_ini = """

View File

@@ -1,4 +1,3 @@
import os
import subprocess
import sys
from pathlib import Path
@@ -39,21 +38,5 @@ def ex_observer():
subprocess.run([sys.executable, str(scriptpath)])
def test_basic():
os.environ["KIND"] = "basic"
def test():
subprocess.run(["tox"])
def test_banana():
os.environ["KIND"] = "banana"
subprocess.run(["tox"])
def test_potato():
os.environ["KIND"] = "potato"
subprocess.run(["tox"])
def test_all():
steps = [test_basic, test_banana, test_potato]
[step() for step in steps]

View File

@@ -1,4 +1,3 @@
import os
import random
import sys
from dataclasses import dataclass
@@ -31,10 +30,8 @@ class Data:
return (2 * self.phys_in) + (8 * self.virt_in)
# get KIND_ID from env var, otherwise set to random
KIND_ID = os.environ.get(
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
)
# let's keep things random
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
vm = voicemeeterlib.api(KIND_ID)
kind = kindmap(KIND_ID)

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 159"><title>tests: 159</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">159</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">159</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 155"><title>tests: 155</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">155</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">155</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 116"><title>tests: 116</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">116</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">116</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 112"><title>tests: 112</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">112</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">112</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 184"><title>tests: 184</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">184</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">184</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 164"><title>tests: 164</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">164</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">164</text></g></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -25,11 +25,7 @@ Function Get-TimeStamp {
if ($MyInvocation.InvocationName -ne ".") {
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
@("potato") | ForEach-Object {
$env:KIND = $_
RunTests
}
RunTests
Invoke-Expression "deactivate"
}

View File

@@ -12,37 +12,37 @@ class TestUserConfigs:
def setup_class(cls):
vm.apply_config("example")
def test_it_tests_vm_config_string(self):
def test_it_vm_config_string(self):
assert "PhysStrip" in vm.strip[data.phys_in].label
assert "VirtStrip" in vm.strip[data.virt_in].label
assert "PhysBus" in vm.bus[data.phys_out].label
assert "VirtBus" in vm.bus[data.virt_out].label
def test_it_tests_vm_config_bool(self):
def test_it_vm_config_bool(self):
assert vm.strip[0].A1 == True
@pytest.mark.skipif(
data.name != "potato",
reason="Skip test if kind is not potato",
)
def test_it_tests_vm_config_bool_strip_eq_on(self):
def test_it_vm_config_bool_strip_eq_on(self):
assert vm.strip[data.phys_in].eq.on == True
@pytest.mark.skipif(
data.name != "banana",
reason="Skip test if kind is not banana",
)
def test_it_tests_vm_config_bool_bus_eq_ab(self):
def test_it_vm_config_bool_bus_eq_ab(self):
assert vm.bus[data.phys_out].eq.ab == True
@pytest.mark.skipif(
"not config.getoption('--run-slow')",
reason="Only run when --run-slow is given",
)
def test_it_tests_vm_config_busmode(self):
def test_it_vm_config_busmode(self):
assert vm.bus[data.phys_out].mode.get() == "composite"
def test_it_tests_vm_config_bass_med_high(self):
def test_it_vm_config_bass_med_high(self):
assert vm.strip[data.virt_in].bass == -3.2
assert vm.strip[data.virt_in].mid == 1.5
assert vm.strip[data.virt_in].high == 2.1

View File

@@ -1,49 +0,0 @@
import re
import pytest
import voicemeeterlib
from tests import data, vm
class TestErrors:
__test__ = True
def test_it_tests_an_unknown_kind(self):
with pytest.raises(
voicemeeterlib.error.VMError,
match="Unknown Voicemeeter kind 'unknown_kind'",
):
voicemeeterlib.api("unknown_kind")
def test_it_tests_an_unknown_parameter(self):
with pytest.raises(
voicemeeterlib.error.CAPIError,
match="VBVMR_SetParameterFloat returned -3",
) as exc_info:
vm.set("unknown.parameter", 1)
e = exc_info.value
assert e.code == -3
assert e.fn_name == "VBVMR_SetParameterFloat"
def test_it_tests_an_unknown_config_name(self):
EXPECTED_MSG = (
"No config with name 'unknown' is loaded into memory",
f"Known configs: {list(vm.configs.keys())}",
)
with pytest.raises(
voicemeeterlib.error.VMError, match=re.escape("\n".join(EXPECTED_MSG))
):
vm.apply_config("unknown")
def test_it_tests_an_invalid_config_key(self):
CONFIG = {
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
"bus-0": {"mute": True, "eq": {"on": True}},
"unknown-0": {"state": True},
"vban-out-1": {"name": "streamname"},
}
with pytest.raises(ValueError, match="invalid config key 'unknown-0'"):
vm.apply(CONFIG)

View File

@@ -10,7 +10,7 @@ class TestRemoteFactories:
data.name != "basic",
reason="Skip test if kind is not basic",
)
def test_it_tests_vm_remote_attrs_for_basic(self):
def test_it_vm_remote_attrs_for_basic(self):
assert hasattr(vm, "strip")
assert hasattr(vm, "bus")
assert hasattr(vm, "command")
@@ -28,7 +28,7 @@ class TestRemoteFactories:
data.name != "banana",
reason="Skip test if kind is not banana",
)
def test_it_tests_vm_remote_attrs_for_banana(self):
def test_it_vm_remote_attrs_for_banana(self):
assert hasattr(vm, "strip")
assert hasattr(vm, "bus")
assert hasattr(vm, "command")
@@ -48,7 +48,7 @@ class TestRemoteFactories:
data.name != "potato",
reason="Skip test if kind is not potato",
)
def test_it_tests_vm_remote_attrs_for_potato(self):
def test_it_vm_remote_attrs_for_potato(self):
assert hasattr(vm, "strip")
assert hasattr(vm, "bus")
assert hasattr(vm, "command")

View File

@@ -210,7 +210,7 @@ class BusLevel(IRemote):
def fget(x):
return round(20 * log(x, 10), 1) if x > 0 else -200.0
if not self._remote.stopped() and self._remote.event.ldirty:
if self._remote.running and self._remote.event.ldirty:
vals = self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
else:
vals = [self._remote.get_level(mode, i) for i in range(*self.range)]
@@ -232,7 +232,7 @@ class BusLevel(IRemote):
Expected to be used in a callback only.
"""
if not self._remote.stopped():
if self._remote.running:
return any(self._remote._bus_comp[self.range[0] : self.range[-1]])
is_updated = isdirty

View File

@@ -121,5 +121,5 @@ class CBindings(metaclass=ABCMeta):
raise CAPIError(func.__name__, res)
return res
except CAPIError as e:
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
self.logger_cbindings.exception(str(e))
raise

View File

@@ -147,13 +147,8 @@ class Loader(metaclass=SingletonType):
self.logger.info(
f"config file with name {identifier} already in memory, skipping.."
)
return
try:
self.parser = dataextraction_factory(data)
except tomllib.TOMLDecodeError as e:
ERR_MSG = (str(e), f"When attempting to load {identifier}.toml")
self.logger.error(f"{type(e).__name__}: {' '.join(ERR_MSG)}")
return
return False
self.parser = dataextraction_factory(data)
return True
def register(self, identifier, data=None):

View File

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

View File

@@ -1,24 +1,19 @@
class VMError(Exception):
"""Base VM Exception class. Raised when general errors occur."""
class InstallError(VMError):
class InstallError(Exception):
"""Exception raised when installation errors occur"""
class CAPIError(VMError):
"""Exception raised when the C-API returns an error code"""
class CAPIError(Exception):
"""Exception raised when the C-API returns error values"""
def __init__(self, fn_name, code):
def __init__(self, fn_name, code, msg=None):
self.fn_name = fn_name
self.code = code
if self.code == -9:
message = " ".join(
(
f"no bind for {self.fn_name}.",
"are you using an old version of the API?",
)
)
else:
message = f"{self.fn_name} returned {self.code}"
super().__init__(message)
self.message = msg if msg else f"{fn_name} returned {code}"
super().__init__(self.message)
def __str__(self):
return f"{type(self).__name__}: {self.message}"
class VMError(Exception):
"""Exception raised when general errors occur"""

View File

@@ -2,7 +2,7 @@ import logging
from abc import abstractmethod
from enum import IntEnum
from functools import cached_property
from typing import Iterable
from typing import Iterable, NoReturn
from . import misc
from .bus import request_bus_obj as bus
@@ -51,7 +51,7 @@ class FactoryBuilder:
)
self.logger = logger.getChild(self.__class__.__name__)
def _pinfo(self, name: str) -> None:
def _pinfo(self, name: str) -> NoReturn:
"""prints progress status for each step"""
name = name.split("_")[1]
self.logger.debug(self._info[int(getattr(self.BuilderProgress, name))])

View File

@@ -12,32 +12,27 @@ if platform.system() != "Windows":
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
REG_KEY = "\\".join(
filter(
None,
(
"SOFTWARE",
"WOW6432Node" if bits == 64 else "",
"Microsoft",
"Windows",
"CurrentVersion",
"Uninstall",
),
)
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("\\".join((REG_KEY, VM_KEY)))
winreg.HKEY_LOCAL_MACHINE, r"{}".format(REG_KEY + "\\" + VM_KEY)
) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
try:
vm_parent = Path(get_vmpath()).parent
vm_path = Path(get_vmpath())
except FileNotFoundError as e:
raise InstallError(f"Unable to fetch DLL path from the registry") from e
vm_parent = vm_path.parent
DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll'

View File

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

View File

@@ -12,9 +12,6 @@ ButtonModes = IntEnum(
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)
@@ -26,7 +23,21 @@ class Adapter(IRemote):
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):

View File

@@ -1,10 +1,9 @@
import ctypes as ct
import logging
import threading
import time
from abc import abstractmethod
from queue import Queue
from typing import Iterable, Optional, Union
from typing import Iterable, NoReturn, Optional, Union
from .cbindings import CBindings
from .error import CAPIError, VMError
@@ -29,11 +28,11 @@ class Remote(CBindings):
self.cache = {}
self.midi = Midi()
self.subject = self.observer = Subject()
self.running = False
self.event = Event(
{k: kwargs.pop(k) for k in ("pdirty", "mdirty", "midi", "ldirty")}
)
self.gui = VmGui()
self.stop_event = None
self.logger = logger.getChild(self.__class__.__name__)
for attr, val in kwargs.items():
@@ -53,21 +52,17 @@ class Remote(CBindings):
def init_thread(self):
"""Starts updates thread."""
self.running = True
self.event.info()
self.logger.debug("initiating events thread")
self.stop_event = threading.Event()
self.stop_event.clear()
queue = Queue()
self.updater = Updater(self, queue)
self.updater.start()
self.producer = Producer(self, queue, self.stop_event)
self.producer = Producer(self, queue)
self.producer.start()
def stopped(self):
return self.stop_event is None or self.stop_event.is_set()
def login(self) -> None:
def login(self) -> NoReturn:
"""Login to the API, initialize dirty parameters"""
self.gui.launched = self.call(self.bind_login, ok=(0, 1)) == 0
if not self.gui.launched:
@@ -80,7 +75,7 @@ class Remote(CBindings):
)
self.clear_dirty()
def run_voicemeeter(self, kind_id: str) -> None:
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 bits == 8:
@@ -121,7 +116,13 @@ class Remote(CBindings):
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}")
raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
ERR_MSG = (
"no bind for VBVMR_MacroButton_IsDirty.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_IsDirty", -9, msg=" ".join(ERR_MSG)
) from e
@property
def ldirty(self) -> bool:
@@ -132,7 +133,7 @@ class Remote(CBindings):
and self.cache.get("bus_level") == self._bus_buf
)
def clear_dirty(self) -> None:
def clear_dirty(self) -> NoReturn:
try:
while self.pdirty or self.mdirty:
pass
@@ -154,7 +155,7 @@ class Remote(CBindings):
self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf))
return buf.value
def set(self, param: str, val: Union[str, float]) -> None:
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:
@@ -181,10 +182,16 @@ class Remote(CBindings):
)
except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
ERR_MSG = (
"no bind for VBVMR_MacroButton_GetStatus.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_GetStatus", -9, msg=" ".join(ERR_MSG)
) from e
return int(c_state.value)
def set_buttonstatus(self, id_: int, val: int, mode: int) -> None:
def set_buttonstatus(self, id_: int, val: int, mode: int) -> NoReturn:
"""Sets a macrobutton parameter. Caches value"""
c_state = ct.c_float(float(val))
try:
@@ -196,7 +203,13 @@ class Remote(CBindings):
)
except AttributeError as e:
self.logger.exception(f"{type(e).__name__}: {e}")
raise CAPIError("VBVMR_MacroButton_SetStatus", -9) from e
ERR_MSG = (
"no bind for VBVMR_MacroButton_SetStatus.",
"are you using an old version of the API?",
)
raise CAPIError(
"VBVMR_MacroButton_SetStatus", -9, msg=" ".join(ERR_MSG)
) from e
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
def get_num_devices(self, direction: str = None) -> int:
@@ -281,25 +294,16 @@ class Remote(CBindings):
minor delay between each recursion
"""
def target(key):
match key.split("-"):
case ["strip" | "bus" | "button" as kls, index] if index.isnumeric():
target = getattr(self, kls)
case [
"vban",
"in" | "instream" | "out" | "outstream" as direction,
index,
] if index.isnumeric():
target = getattr(
self.vban, f"{direction.removesuffix('stream')}stream"
)
case _:
ERR_MSG = f"invalid config key '{key}'"
self.logger.error(ERR_MSG)
raise ValueError(ERR_MSG)
return target[int(index)]
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]
raise ValueError(obj)
[target(key).apply(di).then_wait() for key, di in data.items()]
[param(key).apply(datum).then_wait() for key, datum in data.items()]
def apply_config(self, name):
"""applies a config from memory"""
@@ -327,18 +331,16 @@ class Remote(CBindings):
self.logger.info(f"Profile '{name}' applied!")
def end_thread(self):
if not self.stopped():
self.logger.debug("events thread shutdown started")
self.stop_event.set()
self.producer.join() # wait for producer thread to complete cycle
self.logger.debug("events thread shutdown started")
self.running = False
def logout(self) -> None:
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) -> None:
def __exit__(self, exc_type, exc_value, exc_traceback) -> NoReturn:
"""teardown procedures"""
self.end_thread()
self.logout()

View File

@@ -415,7 +415,7 @@ class StripLevel(IRemote):
def fget(x):
return round(20 * log(x, 10), 1) if x > 0 else -200.0
if not self._remote.stopped() and self._remote.event.ldirty:
if self._remote.running and self._remote.event.ldirty:
vals = self._remote.cache["strip_level"][self.range[0] : self.range[-1]]
else:
vals = [self._remote.get_level(mode, i) for i in range(*self.range)]
@@ -448,7 +448,7 @@ class StripLevel(IRemote):
Expected to be used in a callback only.
"""
if not self._remote.stopped():
if self._remote.running:
return any(self._remote._strip_comp[self.range[0] : self.range[-1]])
is_updated = isdirty

View File

@@ -10,18 +10,14 @@ logger = logging.getLogger(__name__)
class Producer(threading.Thread):
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
def __init__(self, remote, queue, stop_event):
super().__init__(name="producer", daemon=False)
def __init__(self, remote, queue):
super().__init__(name="producer", daemon=True)
self._remote = remote
self.queue = queue
self.stop_event = stop_event
self.logger = logger.getChild(self.__class__.__name__)
def stopped(self):
return self.stop_event.is_set()
def run(self):
while not self.stopped():
while self._remote.running:
if self._remote.event.pdirty:
self.queue.put("pdirty")
if self._remote.event.mdirty:
@@ -60,7 +56,12 @@ class Updater(threading.Thread):
Generate _strip_comp, _bus_comp and update level cache if ldirty.
"""
while event := self.queue.get():
while True:
event = self.queue.get()
if event is None:
self.logger.debug(f"terminating {self.name} thread")
break
if event == "pdirty" and self._remote.pdirty:
self._remote.subject.notify(event)
elif event == "mdirty" and self._remote.mdirty:
@@ -72,4 +73,3 @@ class Updater(threading.Thread):
self._remote.cache["strip_level"] = self._remote._strip_buf
self._remote.cache["bus_level"] = self._remote._bus_buf
self._remote.subject.notify(event)
self.logger.debug(f"terminating {self.name} thread")

View File

@@ -172,24 +172,32 @@ class VbanMidiOutstream(VbanOutstream):
def _make_stream_pair(remote, kind):
num_instream, num_outstream, num_midi, num_text = kind.vban
def _make_cls(i, direction):
match direction:
case "in":
if i < num_instream:
return VbanAudioInstream(remote, i)
elif i < num_instream + num_midi:
return VbanMidiInstream(remote, i)
else:
return VbanTextInstream(remote, i)
case "out":
if i < num_outstream:
return VbanAudioOutstream(remote, i)
else:
return VbanMidiOutstream(remote, i)
def _generate_streams(i, dir):
"""generator function for instream/outstream types"""
if dir == "in":
if i < num_instream:
yield VbanAudioInstream
elif i < num_instream + num_midi:
yield VbanMidiInstream
else:
yield VbanTextInstream
else:
if i < num_outstream:
yield VbanAudioOutstream
else:
yield VbanMidiOutstream
return (
tuple(_make_cls(i, "in") for i in range(num_instream + num_midi + num_text)),
tuple(_make_cls(i, "out") for i in range(num_outstream + num_midi)),
tuple(
cls(remote, i)
for i in range(num_instream + num_midi + num_text)
for cls in _generate_streams(i, "in")
),
tuple(
cls(remote, i)
for i in range(num_outstream + num_midi)
for cls in _generate_streams(i, "out")
),
)