12 Commits

Author SHA1 Message Date
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
11 changed files with 80 additions and 74 deletions

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.4"
description = "A Python wrapper for the Voiceemeter API"
authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT"

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

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

@@ -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:
@@ -331,16 +336,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, dir):
match dir:
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)),
)