Compare commits

..

No commits in common. "770a7742a20ea0af1801a48de48897fd21fe8542" and "c9ae271cf48db4e04ca004f9cf1c5e187755d734" have entirely different histories.

10 changed files with 69 additions and 272 deletions

View File

@ -1,49 +1,3 @@
# NVDA Addon Voicemeeter # NVDA Addon Voicemeeter
Control Voicemeeter GUI with customisable hotkeys. Control Voicemeeter GUI with customisable hotkeys.
## Keybinds
### Controllers
- `NVDA+alt+s`: Enable strip mode
- `NVDA+alt+b`: Enable bus mode.
- `NVDA+alt+1`: Enable controller for channel 1 (strip|bus)
- `NVDA+alt+2`: Enable controller for channel 2 (strip|bus)
- `NVDA+alt+3`: Enable controller for channel 3 (strip|bus)
- `NVDA+alt+4`: Enable controller for channel 4 (strip|bus)
- `NVDA+alt+5`: Enable controller for channel 5 (strip|bus)
- `NVDA+alt+6`: Enable controller for channel 6 (strip|bus)
- `NVDA+alt+7`: Enable controller for channel 7 (strip|bus)
- `NVDA+alt+8`: Enable controller for channel 8 (strip|bus)
### Slider Modes
- `NVDA+alt+g`: Enable gain slider mode.
- `NVDA+alt+c`: Enable comp slider mode.
- `NVDA+alt+t`: Enable gate sldier mode.
- `NVDA+alt+d`: Enable denoiser slider mode.
- `NVDA+alt+a`: Enable audibility slider mode.
### Sliders
- `NVDA+shift+upArrow`: Move slider up by 1 step
- `NVDA+shift+downArrow`: Move slider down by 1 step
- `NVDA+shift+alt+upArrow`: Move slider up by 0.1 step
- `NVDA+shift+alt+downArrow`: Move slider down by 0.1 step
- `NVDA+shift+control+upArrow`: Move slider up by 3 steps
- `NVDA+shift+control+downArrow`: Move slider down by 3 steps
### Channel Parameters
- `NVDA+shift+o`: Mono
- `NVDA+shift+s`: Solo
- `NVDA+shift+m`: Mute
- `NVDA+shift+c`: MC
- `NVDA+shift+k`: Karaoke
### Announcements
- `NVDA+shift+q`: Announce current controller.
- `NVDA+shift+a`: Announce Voicemeeter kind.

View File

@ -14,24 +14,17 @@ def _make_gestures():
defaults = { defaults = {
"kb:NVDA+alt+s": "strip_mode", "kb:NVDA+alt+s": "strip_mode",
"kb:NVDA+alt+b": "bus_mode", "kb:NVDA+alt+b": "bus_mode",
"kb:NVDA+alt+g": "slider_mode",
"kb:NVDA+alt+c": "slider_mode",
"kb:NVDA+alt+t": "slider_mode",
"kb:NVDA+alt+d": "slider_mode",
"kb:NVDA+alt+a": "slider_mode",
"kb:NVDA+shift+q": "announce_controller", "kb:NVDA+shift+q": "announce_controller",
"kb:NVDA+shift+a": "announce_voicemeeter_version", "kb:NVDA+shift+a": "announce_voicemeeter_version",
"kb:NVDA+shift+o": "toggle_mono", "kb:NVDA+shift+o": "toggle_mono",
"kb:NVDA+shift+s": "toggle_solo", "kb:NVDA+shift+s": "toggle_solo",
"kb:NVDA+shift+m": "toggle_mute", "kb:NVDA+shift+m": "toggle_mute",
"kb:NVDA+shift+c": "toggle_mc", "kb:NVDA+shift+upArrow": "increase_gain",
"kb:NVDA+shift+k": "karaoke", "kb:NVDA+shift+downArrow": "decrease_gain",
"kb:NVDA+shift+upArrow": "slider_increase", "kb:NVDA+shift+alt+upArrow": "increase_gain",
"kb:NVDA+shift+downArrow": "slider_decrease", "kb:NVDA+shift+alt+downArrow": "decrease_gain",
"kb:NVDA+shift+alt+upArrow": "slider_increase", "kb:NVDA+shift+control+upArrow": "increase_gain",
"kb:NVDA+shift+alt+downArrow": "slider_decrease", "kb:NVDA+shift+control+downArrow": "decrease_gain",
"kb:NVDA+shift+control+upArrow": "slider_increase",
"kb:NVDA+shift+control+downArrow": "slider_decrease",
} }
overrides = None overrides = None

View File

@ -1,41 +1,51 @@
import ctypes as ct import ctypes as ct
from ctypes.wintypes import CHAR, FLOAT, LONG import winreg
from pathlib import Path
from .cdll import libc from .error import VMError
from .error import VMCAPIError
class Binds: class Binds:
bind_login = libc.VBVMR_Login VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
bind_login.restype = LONG BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
bind_login.argtypes = None
bind_logout = libc.VBVMR_Logout def __init__(self):
bind_logout.restype = LONG dll_path = Path(self.__vmpath()).parent.joinpath(
bind_logout.argtypes = None f'VoicemeeterRemote{"64" if self.BITS == 64 else ""}.dll'
)
if self.BITS == 64:
self.libc = ct.CDLL(str(dll_path))
else:
self.libc = ct.WinDLL(str(dll_path))
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter def __vmpath(self):
bind_run_voicemeeter.restype = LONG with winreg.OpenKey(
bind_run_voicemeeter.argtypes = [LONG] winreg.HKEY_LOCAL_MACHINE,
r"{}".format(
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType "\\".join(
bind_get_voicemeeter_type.restype = LONG (
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)] "\\".join(
filter(
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty None,
bind_is_parameters_dirty.restype = LONG (
bind_is_parameters_dirty.argtypes = None "SOFTWARE",
"WOW6432Node" if self.BITS == 64 else "",
bind_get_parameter_float = libc.VBVMR_GetParameterFloat "Microsoft",
bind_get_parameter_float.restype = LONG "Windows",
bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)] "CurrentVersion",
"Uninstall",
bind_set_parameter_float = libc.VBVMR_SetParameterFloat ),
bind_set_parameter_float.restype = LONG )
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT] ),
self.VM_KEY,
)
)
),
) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
def call(self, fn, *args, ok=(0,)): def call(self, fn, *args, ok=(0,)):
retval = fn(*args) retval = getattr(self.libc, fn)(*args)
if retval not in ok: if retval not in ok:
raise VMCAPIError(fn.__name__, retval) raise VMError(f"{fn} returned {retval}")
return retval return retval

View File

@ -1,49 +0,0 @@
import ctypes as ct
import platform
import winreg
from pathlib import Path
from .error import VMError
BITS = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
if platform.system() != "Windows":
raise VMError("Only Windows OS supported")
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
REG_KEY = "\\".join(
filter(
None,
(
"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)))) as vm_key:
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
try:
vm_parent = Path(get_vmpath()).parent
except FileNotFoundError as e:
raise VMError(f"Unable to fetch DLL path from the registry") from e
DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll'
dll_path = vm_parent.joinpath(DLL_NAME)
if not dll_path.is_file():
raise VMError(f"Could not find {dll_path}")
if BITS == 64:
libc = ct.CDLL(str(dll_path))
else:
libc = ct.WinDLL(str(dll_path))

View File

@ -1,7 +1,7 @@
import ui import ui
from logHandler import log from logHandler import log
from . import context, util from . import context
class CommandsMixin: class CommandsMixin:
@ -37,20 +37,7 @@ class CommandsMixin:
ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}") ui.message(f"Controller for {self.controller.ctx.strategy} {self.controller.ctx.index + 1}")
log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode") log.info(f"INFO - {self.controller.ctx.strategy} {self.controller.ctx.index} mode")
def script_slider_mode(self, gesture): ### BOOLEAN PARMETERS ###
if gesture.displayName.endswith("g"):
self.controller.ctx.slider_mode = "gain"
elif gesture.displayName.endswith("c"):
self.controller.ctx.slider_mode = "comp"
elif gesture.displayName.endswith("t"):
self.controller.ctx.slider_mode = "gate"
elif gesture.displayName.endswith("d"):
self.controller.ctx.slider_mode = "denoiser"
elif gesture.displayName.endswith("a"):
self.controller.ctx.slider_mode = "audibility"
ui.message(f"{self.controller.ctx.slider_mode} mode enabled")
### BOOLEAN PARAMETERS ###
def script_toggle_mono(self, _): def script_toggle_mono(self, _):
val = not self.controller.ctx.get_bool("mono") val = not self.controller.ctx.get_bool("mono")
@ -67,18 +54,6 @@ class CommandsMixin:
self.controller.ctx.set_bool("mute", val) self.controller.ctx.set_bool("mute", val)
ui.message("on" if val else "off") ui.message("on" if val else "off")
def script_toggle_mc(self, _):
val = not self.controller.ctx.get_bool("mc")
self.controller.ctx.set_bool("mc", val)
ui.message("on" if val else "off")
def script_karaoke(self, _):
val = self.controller.ctx.get_int("karaoke") + 1
if val == 5:
val = 0
self.controller.ctx.set_int("karaoke", val)
ui.message(val)
def script_bus_assignment(self, gesture): def script_bus_assignment(self, gesture):
proposed = int(gesture.displayName[-1]) proposed = int(gesture.displayName[-1])
if proposed - 1 < self.kind.phys_out: if proposed - 1 < self.kind.phys_out:
@ -88,29 +63,3 @@ class CommandsMixin:
val = not self.controller.ctx.get_bool(output) val = not self.controller.ctx.get_bool(output)
self.controller.ctx.set_bool(output, val) self.controller.ctx.set_bool(output, val)
ui.message("on" if val else "off") ui.message("on" if val else "off")
### SLIDER MODES ###
def script_slider_increase(self, gesture):
op = util.remove_prefix(gesture.displayName, "kb:NVDA+shift+")
if op.startswith("alt"):
offset = 0.1
elif op.startswith("ctrl"):
offset = 3
else:
offset = 1
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) + offset
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))
def script_slider_decrease(self, gesture):
op = util.remove_prefix(gesture.displayName, "kb:NVDA+shift+")
if op.startswith("alt"):
offset = 0.1
elif op.startswith("ctrl"):
offset = 3
else:
offset = 1
val = self.controller.ctx.get_float(self.controller.ctx.slider_mode) - offset
self.controller.ctx.set_float(self.controller.ctx.slider_mode, val)
ui.message(str(round(val, 1)))

View File

@ -5,15 +5,6 @@ class Strategy(ABC):
def __init__(self, controller, index): def __init__(self, controller, index):
self._controller = controller self._controller = controller
self._index = index self._index = index
self._slider_mode = "gain"
@abstractmethod
def __str__(self):
pass
@property
def identifier(self):
return f"{self}[{self._index}]"
@property @property
def index(self): def index(self):
@ -23,42 +14,30 @@ class Strategy(ABC):
def index(self, val): def index(self, val):
self._index = val self._index = val
@property
def slider_mode(self):
return self._slider_mode
@slider_mode.setter
def slider_mode(self, val):
self._slider_mode = val
def get_bool(self, param: str) -> bool: def get_bool(self, param: str) -> bool:
return self._controller._get(f"{self.identifier}.{param}") == 1 return self._controller._get(f"{self.identifier}.{param}") == 1
def set_bool(self, param: str, val: bool): def set_bool(self, param: str, val: bool):
self._controller._set(f"{self.identifier}.{param}", 1 if val else 0) self._controller._set(f"{self.identifier}.{param}", 1 if val else 0)
def get_float(self, param: str) -> float:
return round(self._controller._get(f"{self.identifier}.{param}"), 1)
def set_float(self, param: str, val: float):
self._controller._set(f"{self.identifier}.{param}", val)
def get_int(self, param: str) -> int:
return int(self._controller._get(f"{self.identifier}.{param}"))
def set_int(self, param: str, val: int):
self._controller._set(f"{self.identifier}.{param}", val)
class StripStrategy(Strategy): class StripStrategy(Strategy):
def __str__(self): def __str__(self):
return "Strip" return "Strip"
@property
def identifier(self):
return f"strip[{self._index}]"
class BusStrategy(Strategy): class BusStrategy(Strategy):
def __str__(self): def __str__(self):
return "Bus" return "Bus"
@property
def identifier(self):
return f"bus[{self._index}]"
class Context: class Context:
def __init__(self, strategy: Strategy) -> None: def __init__(self, strategy: Strategy) -> None:
@ -80,28 +59,8 @@ class Context:
def index(self, val): def index(self, val):
self._strategy._index = val self._strategy._index = val
@property def get_bool(self, *args):
def slider_mode(self):
return self._strategy._slider_mode
@slider_mode.setter
def slider_mode(self, val):
self._strategy._slider_mode = val
def get_bool(self, *args) -> bool:
return self._strategy.get_bool(*args) return self._strategy.get_bool(*args)
def set_bool(self, *args): def set_bool(self, *args):
self._strategy.set_bool(*args) self._strategy.set_bool(*args)
def get_float(self, *args) -> float:
return self._strategy.get_float(*args)
def set_float(self, *args):
self._strategy.set_float(*args)
def get_int(self, *args) -> int:
return self._strategy.get_int(*args)
def set_int(self, *args):
self._strategy.set_int(*args)

View File

@ -3,45 +3,45 @@ import ctypes as ct
from logHandler import log from logHandler import log
from .binds import Binds from .binds import Binds
from .cdll import BITS
from .context import Context, StripStrategy from .context import Context, StripStrategy
from .kinds import KindId from .kinds import KindId
class Controller(Binds): class Controller:
def __init__(self): def __init__(self):
self.binds = Binds()
self.ctx = Context(StripStrategy(self, 0)) self.ctx = Context(StripStrategy(self, 0))
def login(self): def login(self):
retval = self.call(self.bind_login, ok=(0, 1)) retval = self.binds.call("VBVMR_Login", ok=(0, 1))
log.info("INFO - logged into Voicemeeter Remote API") log.info("INFO - logged into Voicemeeter Remote API")
return retval return retval
def logout(self): def logout(self):
self.call(self.bind_logout) self.binds.call("VBVMR_Logout")
log.info("NFO - logged out of Voicemeeter Remote API") log.info("NFO - logged out of Voicemeeter Remote API")
@property @property
def kind_id(self): def kind_id(self):
c_type = ct.c_long() c_type = ct.c_long()
self.call(self.bind_get_voicemeeter_type, ct.byref(c_type)) self.binds.call("VBVMR_GetVoicemeeterType", ct.byref(c_type))
return KindId(c_type.value).name.lower() return KindId(c_type.value).name.lower()
def run_voicemeeter(self, kind_id): def run_voicemeeter(self, kind_id):
val = kind_id.value val = kind_id.value
if val == 3 and BITS == 64: if val == 3 and Binds.BITS == 64:
val = 6 val = 6
self.call(self.bind_run_voicemeeter, val) self.binds.call("VBVMR_RunVoicemeeter", val)
def __clear(self): def __clear(self):
while self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1: while self.binds.call("VBVMR_IsParametersDirty", ok=(0, 1)) == 1:
pass pass
def _get(self, param): def _get(self, param):
self.__clear() self.__clear()
buf = ct.c_float() buf = ct.c_float()
self.call(self.bind_get_parameter_float, param.encode(), ct.byref(buf)) self.binds.call("VBVMR_GetParameterFloat", param.encode(), ct.byref(buf))
return buf.value return buf.value
def _set(self, param, val): def _set(self, param, val):
self.call(self.bind_set_parameter_float, param.encode(), ct.c_float(float(val))) self.binds.call("VBVMR_SetParameterFloat", param.encode(), ct.c_float(float(val)))

View File

@ -1,11 +1,2 @@
class VMError(Exception): class VMError(Exception):
"""Base voicemeeterlib exception class.""" """Base VMError class"""
class VMCAPIError(VMError):
"""Exception raised when the C-API returns an error code"""
def __init__(self, fn_name, code):
self.fn_name = fn_name
self.code = code
super().__init__(f"{self.fn_name} returned {self.code}")

View File

@ -1,10 +0,0 @@
def remove_prefix(input_string, prefix):
if prefix and input_string.startswith(prefix):
return input_string[len(prefix) :]
return input_string
def remove_suffix(input_string, suffix):
if suffix and input_string.endswith(suffix):
return input_string[: -len(suffix)]
return input_string

View File

@ -28,7 +28,7 @@ addon_info = {
The add-on requires Voicemeeter to be installed.""" The add-on requires Voicemeeter to be installed."""
), ),
# version # version
"addon_version": "0.4", "addon_version": "0.1",
# Author(s) # Author(s)
"addon_author": "onyx-and-iris <code@onyxandiris.online>", "addon_author": "onyx-and-iris <code@onyxandiris.online>",
# URL for the add-on documentation support # URL for the add-on documentation support