21 Commits

Author SHA1 Message Date
e9d1e7ffa2 check vban direction
check that index is numeric

patch bump
2023-08-10 21:07:29 +01:00
08525b086c patch bump 2023-08-10 16:28:59 +01:00
cf88b0a63b add poetry test scripts for each kind 2023-08-10 16:27:49 +01:00
4a397d8d96 avoid using keyword as variable name 2023-08-10 16:25:31 +01:00
65fb8990c9 make better use of pattern matching features
error test updated
2023-08-10 16:24:30 +01:00
8c220eb491 refactor target
add error test for ValueError

test badges updated

patch bump
2023-08-09 16:37:10 +01:00
e38922d8a0 upd test badges 2023-08-07 15:49:44 +01:00
96f3faae28 add error tests 2023-08-07 15:39:45 +01:00
bd57f78e8f fix test names 2023-08-07 15:39:22 +01:00
708a7e6d8e reword 2023-08-05 13:05:29 +01:00
409d2deea9 patch bump 2023-08-05 13:02:56 +01:00
0ee3a223ec stopped() checks if stop_event object is None.
In case the events thread was not initiated.
2023-08-05 13:02:04 +01:00
6bfd18c1ac call on_midi_press()
only if midi.current == MIDI_BUTTON
2023-08-05 13:00:45 +01:00
103355d265 use Threading.Event object to terminate producer 2023-08-04 23:13:54 +01:00
09cb62ecfa patch bump 2023-08-04 16:21:21 +01:00
cddd04974b use walrus 2023-08-04 16:21:07 +01:00
50e95d6b8d remove unused imports 2023-08-04 15:19:49 +01:00
b33926f304 replace generator function with factory function
patch bump
2023-08-03 12:09:34 +01:00
58a26e89a8 Correct type annotations None type.
Fixes 'code unreachable'
2023-08-02 17:17:59 +01:00
e96151cd5a InstallError and CAPIError classes
now subclass VMError

minor version bump
2023-08-02 15:42:45 +01:00
6b79c091e8 should the loader attempt to load an invalid toml config
log as error but allow the loader to continue

patch bump
2023-08-01 18:18:02 +01:00
24 changed files with 194 additions and 119 deletions

View File

@@ -380,13 +380,12 @@ 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[4].color = 1
vm.button[55].trigger = False
```
### Recorder

View File

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

View File

@@ -14,19 +14,20 @@ class App:
self.vm.observer.add(self.on_midi)
def on_midi(self):
self.get_info()
self.on_midi_press()
if self.get_info() == self.MIDI_BUTTON:
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 strip 3 level max > -40 and midi button 48 is pressed, then set trigger for macrobutton 0"""
"""if midi button 48 is pressed and strip 3 level max > -40, then set trigger for macrobutton 0"""
if (
max(self.vm.strip[3].levels.postfader) > -40
and self.vm.midi.get(self.MIDI_BUTTON) == 127
self.vm.midi.get(self.MIDI_BUTTON) == 127
and max(self.vm.strip[3].levels.postfader) > -40
):
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.3.6"
version = "2.4.7"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"
@@ -33,7 +33,10 @@ levels = "scripts:ex_levels"
midi = "scripts:ex_midi"
obs = "scripts:ex_obs"
observer = "scripts:ex_observer"
test = "scripts:test"
basic = "scripts:test_basic"
banana = "scripts:test_banana"
potato = "scripts:test_potato"
all = "scripts:test_all"
[tool.tox]
legacy_tox_ini = """

View File

@@ -1,3 +1,4 @@
import os
import subprocess
import sys
from pathlib import Path
@@ -38,5 +39,21 @@ def ex_observer():
subprocess.run([sys.executable, str(scriptpath)])
def test():
def test_basic():
os.environ["KIND"] = "basic"
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,3 +1,4 @@
import os
import random
import sys
from dataclasses import dataclass
@@ -30,8 +31,10 @@ class Data:
return (2 * self.phys_in) + (8 * self.virt_in)
# let's keep things random
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
# 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))
)
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: 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>
<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>

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: 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>
<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>

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: 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>
<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>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

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

View File

@@ -12,37 +12,37 @@ class TestUserConfigs:
def setup_class(cls):
vm.apply_config("example")
def test_it_vm_config_string(self):
def test_it_tests_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_vm_config_bool(self):
def test_it_tests_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_vm_config_bool_strip_eq_on(self):
def test_it_tests_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_vm_config_bool_bus_eq_ab(self):
def test_it_tests_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_vm_config_busmode(self):
def test_it_tests_vm_config_busmode(self):
assert vm.bus[data.phys_out].mode.get() == "composite"
def test_it_vm_config_bass_med_high(self):
def test_it_tests_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

47
tests/test_errors.py Normal file
View File

@@ -0,0 +1,47 @@
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) as exc_info:
vm.apply_config("unknown")
e = exc_info.value
assert e.message == "\n".join(EXPECTED_MSG)
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_vm_remote_attrs_for_basic(self):
def test_it_tests_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_vm_remote_attrs_for_banana(self):
def test_it_tests_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_vm_remote_attrs_for_potato(self):
def test_it_tests_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 self._remote.running and self._remote.event.ldirty:
if not self._remote.stopped() 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 self._remote.running:
if not self._remote.stopped():
return any(self._remote._bus_comp[self.range[0] : self.range[-1]])
is_updated = isdirty

View File

@@ -147,8 +147,13 @@ class Loader(metaclass=SingletonType):
self.logger.info(
f"config file with name {identifier} already in memory, skipping.."
)
return False
self.parser = dataextraction_factory(data)
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 True
def register(self, identifier, data=None):

View File

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

View File

@@ -1,19 +1,22 @@
class InstallError(Exception):
"""Exception raised when installation errors occur"""
class VMError(Exception):
"""Base VM Exception class. Raised when general errors occur."""
class CAPIError(Exception):
"""Exception raised when the C-API returns error values"""
def __init__(self, fn_name, code, msg=None):
self.fn_name = fn_name
self.code = code
self.message = msg if msg else f"{fn_name} returned {code}"
def __init__(self, msg):
self.message = msg
super().__init__(self.message)
def __str__(self):
return f"{type(self).__name__}: {self.message}"
class VMError(Exception):
"""Exception raised when general errors occur"""
class InstallError(VMError):
"""Exception raised when installation errors occur"""
class CAPIError(VMError):
"""Exception raised when the C-API returns an error code"""
def __init__(self, fn_name, code, msg=None):
self.fn_name = fn_name
self.code = code
super(CAPIError, self).__init__(msg if msg else f"{fn_name} returned {code}")

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, NoReturn
from typing import Iterable
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) -> NoReturn:
def _pinfo(self, name: str) -> None:
"""prints progress status for each step"""
name = name.split("_")[1]
self.logger.debug(self._info[int(getattr(self.BuilderProgress, name))])

View File

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

View File

@@ -12,6 +12,9 @@ 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)
@@ -23,21 +26,7 @@ class Adapter(IRemote):
self._remote.set_buttonstatus(self.index, val, mode)
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):
class MacroButton(Adapter):
"""Defines concrete implementation for macrobutton"""
def __str__(self):

View File

@@ -1,9 +1,10 @@
import ctypes as ct
import logging
import threading
import time
from abc import abstractmethod
from queue import Queue
from typing import Iterable, NoReturn, Optional, Union
from typing import Iterable, Optional, Union
from .cbindings import CBindings
from .error import CAPIError, VMError
@@ -28,11 +29,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():
@@ -52,17 +53,21 @@ 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.producer = Producer(self, queue, self.stop_event)
self.producer.start()
def login(self) -> NoReturn:
def stopped(self):
return self.stop_event is None or self.stop_event.is_set()
def login(self) -> None:
"""Login to the API, initialize dirty parameters"""
self.gui.launched = self.call(self.bind_login, ok=(0, 1)) == 0
if not self.gui.launched:
@@ -75,7 +80,7 @@ class Remote(CBindings):
)
self.clear_dirty()
def run_voicemeeter(self, kind_id: str) -> NoReturn:
def run_voicemeeter(self, kind_id: str) -> None:
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:
@@ -133,7 +138,7 @@ class Remote(CBindings):
and self.cache.get("bus_level") == self._bus_buf
)
def clear_dirty(self) -> NoReturn:
def clear_dirty(self) -> None:
try:
while self.pdirty or self.mdirty:
pass
@@ -155,7 +160,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]) -> NoReturn:
def set(self, param: str, val: Union[str, float]) -> None:
"""Sets a string or float parameter. Caches value"""
if isinstance(val, str):
if len(val) >= 512:
@@ -191,7 +196,7 @@ class Remote(CBindings):
) from e
return int(c_state.value)
def set_buttonstatus(self, id_: int, val: int, mode: int) -> NoReturn:
def set_buttonstatus(self, id_: int, val: int, mode: int) -> None:
"""Sets a macrobutton parameter. Caches value"""
c_state = ct.c_float(float(val))
try:
@@ -294,16 +299,25 @@ class Remote(CBindings):
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]
raise ValueError(obj)
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)]
[param(key).apply(datum).then_wait() for key, datum in data.items()]
[target(key).apply(di).then_wait() for key, di in data.items()]
def apply_config(self, name):
"""applies a config from memory"""
@@ -331,16 +345,18 @@ class Remote(CBindings):
self.logger.info(f"Profile '{name}' applied!")
def end_thread(self):
self.logger.debug("events thread shutdown started")
self.running = False
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
def logout(self) -> NoReturn:
def logout(self) -> None:
"""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:
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
"""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 self._remote.running and self._remote.event.ldirty:
if not self._remote.stopped() 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 self._remote.running:
if not self._remote.stopped():
return any(self._remote._strip_comp[self.range[0] : self.range[-1]])
is_updated = isdirty

View File

@@ -10,14 +10,18 @@ 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):
super().__init__(name="producer", daemon=True)
def __init__(self, remote, queue, stop_event):
super().__init__(name="producer", daemon=False)
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 self._remote.running:
while not self.stopped():
if self._remote.event.pdirty:
self.queue.put("pdirty")
if self._remote.event.mdirty:
@@ -56,12 +60,7 @@ class Updater(threading.Thread):
Generate _strip_comp, _bus_comp and update level cache if ldirty.
"""
while True:
event = self.queue.get()
if event is None:
self.logger.debug(f"terminating {self.name} thread")
break
while event := self.queue.get():
if event == "pdirty" and self._remote.pdirty:
self._remote.subject.notify(event)
elif event == "mdirty" and self._remote.mdirty:
@@ -73,3 +72,4 @@ 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,32 +172,24 @@ class VbanMidiOutstream(VbanOutstream):
def _make_stream_pair(remote, kind):
num_instream, num_outstream, num_midi, num_text = kind.vban
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
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)
return (
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")
),
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)),
)