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]
|
- [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
|
## [2.3.2] - 2023-07-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -842,10 +842,13 @@ True iff a level has been updated.
|
|||||||
|
|
||||||
### Errors
|
### Errors
|
||||||
|
|
||||||
- `errors.VMError`: Exception raised when general errors occur.
|
- `errors.VMError`: Base custom exception class.
|
||||||
- `errors.InstallError`: Exception raised when installation errors occur.
|
- `errors.InstallError`: Exception raised when installation errors occur.
|
||||||
- `errors.CAPIError`: Exception raised when the C-API returns error values.
|
- `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
|
### Logging
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
from pyparsing import (
|
from pyparsing import (
|
||||||
Combine,
|
Combine,
|
||||||
@@ -10,24 +12,88 @@ from pyparsing import (
|
|||||||
Suppress,
|
Suppress,
|
||||||
Word,
|
Word,
|
||||||
alphanums,
|
alphanums,
|
||||||
alphas,
|
|
||||||
nums,
|
nums,
|
||||||
)
|
)
|
||||||
|
|
||||||
import voicemeeterlib
|
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 = argparse.ArgumentParser(description="creates a basic dsl")
|
||||||
argparser.add_argument("-i", action="store_true")
|
argparser.add_argument("-i", action="store_true")
|
||||||
args = argparser.parse_args()
|
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:
|
class Parser:
|
||||||
|
IS_STRING = ("label",)
|
||||||
|
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.vm = vm
|
self.vm = vm
|
||||||
self.kls = Group(OneOrMore(Word(alphanums)))
|
self.kls = Group(OneOrMore(Word(alphanums)))
|
||||||
self.token = Suppress("->")
|
self.token = Suppress("->")
|
||||||
self.param = Word(alphanums)
|
self.param = Group(OneOrMore(Word(alphanums)))
|
||||||
self.value = Combine(
|
self.value = Combine(
|
||||||
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
|
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
|
||||||
) | Group(OneOrMore(Word(alphanums)))
|
) | Group(OneOrMore(Word(alphanums)))
|
||||||
@@ -39,27 +105,64 @@ class Parser:
|
|||||||
+ Optional(self.value)
|
+ Optional(self.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, cmds):
|
def converter(self, cmds):
|
||||||
|
"""determines the kind of parameter from the parsed string"""
|
||||||
|
|
||||||
res = list()
|
res = list()
|
||||||
|
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
if len(self.event.parseString(cmd)) == 2:
|
self.logger.debug(f"running command: {cmd}")
|
||||||
kls, param = self.event.parseString(cmd)
|
match cmd_parsed := self.event.parseString(cmd):
|
||||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
case [[kls, index], [param]]:
|
||||||
res.append(getattr(target, param))
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
elif len(self.event.parseString(cmd)) == 3:
|
res.append(getattr(target, param))
|
||||||
kls, param, val = self.event.parseString(cmd)
|
case [[kls, index], [param], val] if param in self.IS_STRING:
|
||||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
if "".join(val) in ["off", "on"]:
|
context = self._get_context(ParamKinds.string, target, param, val)
|
||||||
setattr(target, param, bool(["off", "on"].index("".join(val))))
|
context.run()
|
||||||
elif param in ["gain", "comp", "gate", "limit", "audibility"]:
|
case [[kls, index], [param], [val] | val]:
|
||||||
setattr(target, param, float("".join(val)))
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
elif param in ["label"]:
|
try:
|
||||||
setattr(target, param, " ".join(val))
|
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)
|
time.sleep(0.05)
|
||||||
return res
|
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):
|
def interactive_mode(parser):
|
||||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||||
@@ -70,25 +173,23 @@ def interactive_mode(parser):
|
|||||||
def main():
|
def main():
|
||||||
# fmt: off
|
# fmt: off
|
||||||
cmds = (
|
cmds = (
|
||||||
"strip 0 -> mute -> on", "strip 0 -> mute", "bus 0 -> mute -> on",
|
"strip 0 -> mute -> true", "strip 0 -> mute", "bus 0 -> mute -> true",
|
||||||
"strip 0 -> mute -> off", "bus 0 -> mute -> on", "strip 3 -> solo -> on",
|
"strip 0 -> mute -> false", "bus 0 -> mute -> true", "strip 3 -> solo -> true",
|
||||||
"strip 3 -> solo -> off", "strip 1 -> A1 -> on", "strip 1 -> A1",
|
"strip 3 -> solo -> false", "strip 1 -> A1 -> true", "strip 1 -> A1",
|
||||||
"strip 1 -> A1 -> off", "strip 1 -> A1", "bus 3 -> eq -> on",
|
"strip 1 -> A1 -> false", "strip 1 -> A1", "strip 3 -> eq on -> true",
|
||||||
"bus 3 -> eq -> off", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
"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 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
|
# fmt: on
|
||||||
|
|
||||||
KIND_ID = "banana"
|
with voicemeeterlib.api("potato") as vm:
|
||||||
|
|
||||||
with voicemeeterlib.api(KIND_ID) as vm:
|
|
||||||
parser = Parser(vm)
|
parser = Parser(vm)
|
||||||
if args.i:
|
if args.i:
|
||||||
interactive_mode(parser)
|
interactive_mode(parser)
|
||||||
return
|
return
|
||||||
|
|
||||||
if res := parser.parse(cmds):
|
if res := parser.converter(cmds):
|
||||||
print(res)
|
print(res)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "voicemeeter-api"
|
name = "voicemeeter-api"
|
||||||
version = "2.4.4"
|
version = "2.4.8"
|
||||||
description = "A Python wrapper for the Voiceemeter API"
|
description = "A Python wrapper for the Voiceemeter API"
|
||||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -33,7 +33,10 @@ levels = "scripts:ex_levels"
|
|||||||
midi = "scripts:ex_midi"
|
midi = "scripts:ex_midi"
|
||||||
obs = "scripts:ex_obs"
|
obs = "scripts:ex_obs"
|
||||||
observer = "scripts:ex_observer"
|
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]
|
[tool.tox]
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
|
|||||||
19
scripts.py
19
scripts.py
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -38,5 +39,21 @@ def ex_observer():
|
|||||||
subprocess.run([sys.executable, str(scriptpath)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def test():
|
def test_basic():
|
||||||
|
os.environ["KIND"] = "basic"
|
||||||
subprocess.run(["tox"])
|
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 random
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -30,8 +31,10 @@ class Data:
|
|||||||
return (2 * self.phys_in) + (8 * self.virt_in)
|
return (2 * self.phys_in) + (8 * self.virt_in)
|
||||||
|
|
||||||
|
|
||||||
# let's keep things random
|
# get KIND_ID from env var, otherwise set to random
|
||||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
KIND_ID = os.environ.get(
|
||||||
|
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||||
|
)
|
||||||
vm = voicemeeterlib.api(KIND_ID)
|
vm = voicemeeterlib.api(KIND_ID)
|
||||||
kind = kindmap(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 ".") {
|
if ($MyInvocation.InvocationName -ne ".") {
|
||||||
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
||||||
|
|
||||||
RunTests
|
@("potato") | ForEach-Object {
|
||||||
|
$env:KIND = $_
|
||||||
|
RunTests
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Invoke-Expression "deactivate"
|
Invoke-Expression "deactivate"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,37 +12,37 @@ class TestUserConfigs:
|
|||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
vm.apply_config("example")
|
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 "PhysStrip" in vm.strip[data.phys_in].label
|
||||||
assert "VirtStrip" in vm.strip[data.virt_in].label
|
assert "VirtStrip" in vm.strip[data.virt_in].label
|
||||||
assert "PhysBus" in vm.bus[data.phys_out].label
|
assert "PhysBus" in vm.bus[data.phys_out].label
|
||||||
assert "VirtBus" in vm.bus[data.virt_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
|
assert vm.strip[0].A1 == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
reason="Skip test if kind is not 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
|
assert vm.strip[data.phys_in].eq.on == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
reason="Skip test if kind is not 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
|
assert vm.bus[data.phys_out].eq.ab == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"not config.getoption('--run-slow')",
|
"not config.getoption('--run-slow')",
|
||||||
reason="Only run when --run-slow is given",
|
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"
|
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].bass == -3.2
|
||||||
assert vm.strip[data.virt_in].mid == 1.5
|
assert vm.strip[data.virt_in].mid == 1.5
|
||||||
assert vm.strip[data.virt_in].high == 2.1
|
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",
|
data.name != "basic",
|
||||||
reason="Skip test if kind is not 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, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
@@ -28,7 +28,7 @@ class TestRemoteFactories:
|
|||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
reason="Skip test if kind is not 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, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
@@ -48,7 +48,7 @@ class TestRemoteFactories:
|
|||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
reason="Skip test if kind is not 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, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
|
|||||||
@@ -121,5 +121,5 @@ class CBindings(metaclass=ABCMeta):
|
|||||||
raise CAPIError(func.__name__, res)
|
raise CAPIError(func.__name__, res)
|
||||||
return res
|
return res
|
||||||
except CAPIError as e:
|
except CAPIError as e:
|
||||||
self.logger_cbindings.exception(str(e))
|
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
class VMError(Exception):
|
class VMError(Exception):
|
||||||
"""Base VM Exception class. Raised when general errors occur."""
|
"""Base voicemeeterlib exception class."""
|
||||||
|
|
||||||
def __init__(self, msg):
|
|
||||||
self.message = msg
|
|
||||||
super().__init__(self.message)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"{type(self).__name__}: {self.message}"
|
|
||||||
|
|
||||||
|
|
||||||
class InstallError(VMError):
|
class InstallError(VMError):
|
||||||
@@ -16,7 +9,16 @@ class InstallError(VMError):
|
|||||||
class CAPIError(VMError):
|
class CAPIError(VMError):
|
||||||
"""Exception raised when the C-API returns an error code"""
|
"""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.fn_name = fn_name
|
||||||
self.code = code
|
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}"
|
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
||||||
REG_KEY = "".join(
|
REG_KEY = "\\".join(
|
||||||
[
|
filter(
|
||||||
"SOFTWARE",
|
None,
|
||||||
("\\WOW6432Node" if bits == 64 else ""),
|
(
|
||||||
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
"SOFTWARE",
|
||||||
]
|
"WOW6432Node" if bits == 64 else "",
|
||||||
|
"Microsoft",
|
||||||
|
"Windows",
|
||||||
|
"CurrentVersion",
|
||||||
|
"Uninstall",
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_vmpath():
|
def get_vmpath():
|
||||||
with winreg.OpenKey(
|
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:
|
) as vm_key:
|
||||||
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
|
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vm_path = Path(get_vmpath())
|
vm_parent = Path(get_vmpath()).parent
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise InstallError(f"Unable to fetch DLL path from the registry") from 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'
|
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
|
return self.call(self.bind_macro_button_is_dirty, ok=(0, 1)) == 1
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
ERR_MSG = (
|
raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
|
||||||
"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
|
@property
|
||||||
def ldirty(self) -> bool:
|
def ldirty(self) -> bool:
|
||||||
@@ -187,13 +181,7 @@ class Remote(CBindings):
|
|||||||
)
|
)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
ERR_MSG = (
|
raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
|
||||||
"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)
|
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) -> None:
|
||||||
@@ -208,13 +196,7 @@ class Remote(CBindings):
|
|||||||
)
|
)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
ERR_MSG = (
|
raise CAPIError("VBVMR_MacroButton_SetStatus", -9) from e
|
||||||
"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)
|
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
|
||||||
|
|
||||||
def get_num_devices(self, direction: str = None) -> int:
|
def get_num_devices(self, direction: str = None) -> int:
|
||||||
@@ -299,16 +281,25 @@ class Remote(CBindings):
|
|||||||
minor delay between each recursion
|
minor delay between each recursion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def param(key):
|
def target(key):
|
||||||
obj, m2, *rem = key.split("-")
|
match key.split("-"):
|
||||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
case ["strip" | "bus" | "button" as kls, index] if index.isnumeric():
|
||||||
if obj in ("strip", "bus", "button"):
|
target = getattr(self, kls)
|
||||||
return getattr(self, obj)[index]
|
case [
|
||||||
elif obj == "vban":
|
"vban",
|
||||||
return getattr(getattr(self, obj), f"{m2}stream")[index]
|
"in" | "instream" | "out" | "outstream" as direction,
|
||||||
raise ValueError(obj)
|
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):
|
def apply_config(self, name):
|
||||||
"""applies a config from memory"""
|
"""applies a config from memory"""
|
||||||
|
|||||||
@@ -172,8 +172,8 @@ class VbanMidiOutstream(VbanOutstream):
|
|||||||
def _make_stream_pair(remote, kind):
|
def _make_stream_pair(remote, kind):
|
||||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||||
|
|
||||||
def _make_cls(i, dir):
|
def _make_cls(i, direction):
|
||||||
match dir:
|
match direction:
|
||||||
case "in":
|
case "in":
|
||||||
if i < num_instream:
|
if i < num_instream:
|
||||||
return VbanAudioInstream(remote, i)
|
return VbanAudioInstream(remote, i)
|
||||||
|
|||||||
Reference in New Issue
Block a user