mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-18 20:43:33 +00:00
Compare commits
15 Commits
add-event-
...
84fdd94559
| Author | SHA1 | Date | |
|---|---|---|---|
| 84fdd94559 | |||
| eb66f9e2bc | |||
| ce7cda100f | |||
| 2bba0ff67a | |||
| df473d89ae | |||
| 5aaa9aab71 | |||
| e9d1e7ffa2 | |||
| 08525b086c | |||
| cf88b0a63b | |||
| 4a397d8d96 | |||
| 65fb8990c9 | |||
| 8c220eb491 | |||
| e38922d8a0 | |||
| 96f3faae28 | |||
| bd57f78e8f |
18
CHANGELOG.md
18
CHANGELOG.md
@@ -11,6 +11,24 @@ 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
|
||||
|
||||
@@ -842,10 +842,13 @@ True iff a level has been updated.
|
||||
|
||||
### Errors
|
||||
|
||||
- `errors.VMError`: Exception raised when general errors occur.
|
||||
- `errors.VMError`: Base custom exception class.
|
||||
- `errors.InstallError`: Exception raised when installation errors occur.
|
||||
- `errors.CAPIError`: Exception raised when the C-API returns error values.
|
||||
- Error codes are stored in {Exception Class}.code. For a full list of error codes [check the VoicemeeterRemote header file][Voicemeeter Remote Header].
|
||||
- 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].
|
||||
|
||||
|
||||
### Logging
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
from enum import IntEnum
|
||||
|
||||
from pyparsing import (
|
||||
Combine,
|
||||
@@ -10,24 +12,88 @@ from pyparsing import (
|
||||
Suppress,
|
||||
Word,
|
||||
alphanums,
|
||||
alphas,
|
||||
nums,
|
||||
)
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
argparser = argparse.ArgumentParser(description="creates a basic dsl")
|
||||
argparser.add_argument("-i", action="store_true")
|
||||
args = argparser.parse_args()
|
||||
|
||||
|
||||
ParamKinds = IntEnum(
|
||||
"ParamKinds",
|
||||
"bool float string",
|
||||
)
|
||||
|
||||
|
||||
class Strategy(ABC):
|
||||
def __init__(self, target, param, val):
|
||||
self.target = target
|
||||
self.param = param
|
||||
self.val = val
|
||||
|
||||
@abstractmethod
|
||||
def run(self):
|
||||
pass
|
||||
|
||||
|
||||
class BoolStrategy(Strategy):
|
||||
def run(self):
|
||||
setattr(self.target, self.param, self.strtobool(self.val))
|
||||
|
||||
def strtobool(self, val):
|
||||
"""Convert a string representation of truth to it's numeric form."""
|
||||
|
||||
val = val.lower()
|
||||
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||
return 1
|
||||
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||
return 0
|
||||
else:
|
||||
raise ValueError("invalid truth value %r" % (val,))
|
||||
|
||||
|
||||
class FloatStrategy(Strategy):
|
||||
def run(self):
|
||||
setattr(self.target, self.param, float(self.val))
|
||||
|
||||
|
||||
class StringStrategy(Strategy):
|
||||
def run(self):
|
||||
setattr(self.target, self.param, " ".join(self.val))
|
||||
|
||||
|
||||
class Context:
|
||||
def __init__(self, strategy: Strategy) -> None:
|
||||
self._strategy = strategy
|
||||
|
||||
@property
|
||||
def strategy(self) -> Strategy:
|
||||
return self._strategy
|
||||
|
||||
@strategy.setter
|
||||
def strategy(self, strategy: Strategy) -> None:
|
||||
self._strategy = strategy
|
||||
|
||||
def run(self):
|
||||
self.strategy.run()
|
||||
|
||||
|
||||
class Parser:
|
||||
IS_STRING = ("label",)
|
||||
|
||||
def __init__(self, vm):
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.vm = vm
|
||||
self.kls = Group(OneOrMore(Word(alphanums)))
|
||||
self.token = Suppress("->")
|
||||
self.param = Word(alphanums)
|
||||
self.param = Group(OneOrMore(Word(alphanums)))
|
||||
self.value = Combine(
|
||||
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
|
||||
) | Group(OneOrMore(Word(alphanums)))
|
||||
@@ -39,27 +105,64 @@ class Parser:
|
||||
+ Optional(self.value)
|
||||
)
|
||||
|
||||
def parse(self, cmds):
|
||||
def converter(self, cmds):
|
||||
"""determines the kind of parameter from the parsed string"""
|
||||
|
||||
res = list()
|
||||
|
||||
for cmd in cmds:
|
||||
if len(self.event.parseString(cmd)) == 2:
|
||||
kls, param = self.event.parseString(cmd)
|
||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
||||
res.append(getattr(target, param))
|
||||
elif len(self.event.parseString(cmd)) == 3:
|
||||
kls, param, val = self.event.parseString(cmd)
|
||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
||||
if "".join(val) in ["off", "on"]:
|
||||
setattr(target, param, bool(["off", "on"].index("".join(val))))
|
||||
elif param in ["gain", "comp", "gate", "limit", "audibility"]:
|
||||
setattr(target, param, float("".join(val)))
|
||||
elif param in ["label"]:
|
||||
setattr(target, param, " ".join(val))
|
||||
|
||||
self.logger.debug(f"running command: {cmd}")
|
||||
match cmd_parsed := self.event.parseString(cmd):
|
||||
case [[kls, index], [param]]:
|
||||
target = getattr(self.vm, kls)[int(index)]
|
||||
res.append(getattr(target, param))
|
||||
case [[kls, index], [param], val] if param in self.IS_STRING:
|
||||
target = getattr(self.vm, kls)[int(index)]
|
||||
context = self._get_context(ParamKinds.string, target, param, val)
|
||||
context.run()
|
||||
case [[kls, index], [param], [val] | val]:
|
||||
target = getattr(self.vm, kls)[int(index)]
|
||||
try:
|
||||
context = self._get_context(ParamKinds.bool, target, param, val)
|
||||
context.run()
|
||||
except ValueError as e:
|
||||
self.logger.error(f"{e}... switching to float strategy")
|
||||
context.strategy = FloatStrategy(target, param, val)
|
||||
context.run()
|
||||
case [
|
||||
[kls, index],
|
||||
[secondary, param],
|
||||
[val] | val,
|
||||
]:
|
||||
primary = getattr(self.vm, kls)[int(index)]
|
||||
target = getattr(primary, secondary)
|
||||
try:
|
||||
context = self._get_context(ParamKinds.bool, target, param, val)
|
||||
context.run()
|
||||
except ValueError as e:
|
||||
self.logger.error(f"{e}... switching to float strategy")
|
||||
context.strategy = FloatStrategy(target, param, val)
|
||||
context.run()
|
||||
case _:
|
||||
self.logger.error(
|
||||
f"unable to determine the kind of parameter from {cmd_parsed}"
|
||||
)
|
||||
time.sleep(0.05)
|
||||
return res
|
||||
|
||||
def _get_context(self, kind, *args):
|
||||
"""
|
||||
determines a strategy for a kind of parameter and passes it to the context.
|
||||
"""
|
||||
|
||||
match kind:
|
||||
case ParamKinds.bool:
|
||||
context = Context(BoolStrategy(*args))
|
||||
case ParamKinds.float:
|
||||
context = Context(FloatStrategy(*args))
|
||||
case ParamKinds.string:
|
||||
context = Context(StringStrategy(*args))
|
||||
return context
|
||||
|
||||
|
||||
def interactive_mode(parser):
|
||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||
@@ -70,25 +173,23 @@ def interactive_mode(parser):
|
||||
def main():
|
||||
# fmt: off
|
||||
cmds = (
|
||||
"strip 0 -> mute -> on", "strip 0 -> mute", "bus 0 -> mute -> on",
|
||||
"strip 0 -> mute -> off", "bus 0 -> mute -> on", "strip 3 -> solo -> on",
|
||||
"strip 3 -> solo -> off", "strip 1 -> A1 -> on", "strip 1 -> A1",
|
||||
"strip 1 -> A1 -> off", "strip 1 -> A1", "bus 3 -> eq -> on",
|
||||
"bus 3 -> eq -> off", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
||||
"strip 0 -> mute -> true", "strip 0 -> mute", "bus 0 -> mute -> true",
|
||||
"strip 0 -> mute -> false", "bus 0 -> mute -> true", "strip 3 -> solo -> true",
|
||||
"strip 3 -> solo -> false", "strip 1 -> A1 -> true", "strip 1 -> A1",
|
||||
"strip 1 -> A1 -> false", "strip 1 -> A1", "strip 3 -> eq on -> true",
|
||||
"bus 3 -> eq on -> false", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
||||
"strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28",
|
||||
"strip 2 -> limit",
|
||||
"strip 2 -> limit", "strip 3 -> comp knob -> 3.8"
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
KIND_ID = "banana"
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vm:
|
||||
with voicemeeterlib.api("potato") as vm:
|
||||
parser = Parser(vm)
|
||||
if args.i:
|
||||
interactive_mode(parser)
|
||||
return
|
||||
|
||||
if res := parser.parse(cmds):
|
||||
if res := parser.converter(cmds):
|
||||
print(res)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "voicemeeter-api"
|
||||
version = "2.4.4"
|
||||
version = "2.4.8"
|
||||
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 = """
|
||||
|
||||
19
scripts.py
19
scripts.py
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
49
tests/test_errors.py
Normal file
49
tests/test_errors.py
Normal file
@@ -0,0 +1,49 @@
|
||||
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)
|
||||
@@ -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")
|
||||
|
||||
@@ -121,5 +121,5 @@ class CBindings(metaclass=ABCMeta):
|
||||
raise CAPIError(func.__name__, res)
|
||||
return res
|
||||
except CAPIError as e:
|
||||
self.logger_cbindings.exception(str(e))
|
||||
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
||||
raise
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
class VMError(Exception):
|
||||
"""Base VM Exception class. Raised when general errors occur."""
|
||||
|
||||
def __init__(self, msg):
|
||||
self.message = msg
|
||||
super().__init__(self.message)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}: {self.message}"
|
||||
"""Base voicemeeterlib exception class."""
|
||||
|
||||
|
||||
class InstallError(VMError):
|
||||
@@ -16,7 +9,16 @@ class InstallError(VMError):
|
||||
class CAPIError(VMError):
|
||||
"""Exception raised when the C-API returns an error code"""
|
||||
|
||||
def __init__(self, fn_name, code, msg=None):
|
||||
def __init__(self, fn_name, code):
|
||||
self.fn_name = fn_name
|
||||
self.code = code
|
||||
super(CAPIError, self).__init__(msg if msg else f"{fn_name} returned {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)
|
||||
|
||||
@@ -12,27 +12,32 @@ if platform.system() != "Windows":
|
||||
|
||||
|
||||
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
||||
REG_KEY = "".join(
|
||||
[
|
||||
"SOFTWARE",
|
||||
("\\WOW6432Node" if bits == 64 else ""),
|
||||
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
||||
]
|
||||
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(REG_KEY + "\\" + VM_KEY)
|
||||
winreg.HKEY_LOCAL_MACHINE, r"{}".format("\\".join((REG_KEY, VM_KEY)))
|
||||
) as vm_key:
|
||||
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
|
||||
|
||||
|
||||
try:
|
||||
vm_path = Path(get_vmpath())
|
||||
vm_parent = Path(get_vmpath()).parent
|
||||
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'
|
||||
|
||||
|
||||
@@ -121,13 +121,7 @@ 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}")
|
||||
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
|
||||
raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
@@ -187,13 +181,7 @@ class Remote(CBindings):
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {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
|
||||
raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
|
||||
return int(c_state.value)
|
||||
|
||||
def set_buttonstatus(self, id_: int, val: int, mode: int) -> None:
|
||||
@@ -208,13 +196,7 @@ class Remote(CBindings):
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {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
|
||||
raise CAPIError("VBVMR_MacroButton_SetStatus", -9) from e
|
||||
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
|
||||
|
||||
def get_num_devices(self, direction: str = None) -> int:
|
||||
@@ -299,16 +281,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"""
|
||||
|
||||
@@ -172,8 +172,8 @@ class VbanMidiOutstream(VbanOutstream):
|
||||
def _make_stream_pair(remote, kind):
|
||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||
|
||||
def _make_cls(i, dir):
|
||||
match dir:
|
||||
def _make_cls(i, direction):
|
||||
match direction:
|
||||
case "in":
|
||||
if i < num_instream:
|
||||
return VbanAudioInstream(remote, i)
|
||||
|
||||
Reference in New Issue
Block a user