mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-06 23:43:30 +00:00
Compare commits
87 Commits
add-to-rec
...
add-bits-k
| Author | SHA1 | Date | |
|---|---|---|---|
| fd571d3b37 | |||
| 1e5d720169 | |||
| 3b48367026 | |||
| d47650b150 | |||
| 174bf2db1f | |||
| b0722af5b7 | |||
| c9f5b680ce | |||
| 8e56689a8f | |||
| 43cb7df5ac | |||
| dcc0fc5ccc | |||
|
|
ea4626b864 | ||
| 9ae0815200 | |||
|
|
fa1f8dd181 | ||
| 25ebfe5a61 | |||
| 6c558421e2 | |||
| 0d6355a6a4 | |||
| a0d657468b | |||
| 94a0e6770c | |||
| d03d19eecd | |||
| ce5936b740 | |||
| 2e1916eeaa | |||
| 54dfa372b1 | |||
| b360545aa6 | |||
| 4bfc32ad91 | |||
| a0eb56a575 | |||
| 23ed41ed4a | |||
| b79e6a9dbe | |||
| b4306d9af5 | |||
| ab23280e70 | |||
| 73482c1f84 | |||
| 84fdd94559 | |||
| eb66f9e2bc | |||
| ce7cda100f | |||
| 2bba0ff67a | |||
| df473d89ae | |||
| 5aaa9aab71 | |||
| e9d1e7ffa2 | |||
| 08525b086c | |||
| cf88b0a63b | |||
| 4a397d8d96 | |||
| 65fb8990c9 | |||
| 8c220eb491 | |||
| e38922d8a0 | |||
| 96f3faae28 | |||
| bd57f78e8f | |||
| 708a7e6d8e | |||
| 409d2deea9 | |||
| 0ee3a223ec | |||
| 6bfd18c1ac | |||
| 103355d265 | |||
| 09cb62ecfa | |||
| cddd04974b | |||
| 50e95d6b8d | |||
| b33926f304 | |||
| 58a26e89a8 | |||
| e96151cd5a | |||
| 6b79c091e8 | |||
| bf77ded007 | |||
| 236125d095 | |||
| 7841dfe10f | |||
| bdf8dc489a | |||
| 160a6f89f9 | |||
| 4fcb2f93ca | |||
| 8acd0b1385 | |||
| 89866bb87b | |||
| f996fc0d9c | |||
| 68177c3c6c | |||
| 54a1938694 | |||
| 9a4205ce64 | |||
| 9b2e38aab3 | |||
| 278566c2e0 | |||
| b0acde6a52 | |||
| 07b04d16d8 | |||
| f854ec7875 | |||
| 5640f54e65 | |||
| 4569e8c760 | |||
| 5e39461966 | |||
| 6de78a4037 | |||
| bafaa58507 | |||
| af368b4b0a | |||
| 32527e37bd | |||
| c21b04e1a8 | |||
| 76960f36d0 | |||
| 2849b37670 | |||
| 7732a26c40 | |||
| c1e23ab250 | |||
| c2daba1a62 |
73
CHANGELOG.md
73
CHANGELOG.md
@@ -11,7 +11,73 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.1.0] - 2023-07-01
|
||||
## [2.6.0] - 2024-06-29
|
||||
|
||||
### Added
|
||||
|
||||
- bits kwarg for overriding the type of GUI that is launched on startup.
|
||||
- Defaults to 64, set it to either 32 or 64.
|
||||
|
||||
### Fixed
|
||||
|
||||
- {Remote}.run_voicemeeter() now launches x64 bit GUI's for all kinds if Python detects a 64 bit system.
|
||||
|
||||
## [2.5.0] - 2023-10-27
|
||||
|
||||
### Fixed
|
||||
|
||||
- {Remote}.login() now has a configurable timeout. Use timeout kwarg to set it. Defaults to 2 seconds.
|
||||
- Remote class section in README updated to include timeout kwarg.
|
||||
|
||||
## [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
|
||||
|
||||
- vban.{instream,outstream} tuples now contain classes that represent MIDI and TEXT streams.
|
||||
|
||||
### Fixed
|
||||
|
||||
- apply_config() now performs a deep merge when extending a config with another.
|
||||
|
||||
## [2.3.0] - 2023-07-11
|
||||
|
||||
### Added
|
||||
|
||||
- user configs may now extend other user configs. check `config extends` section in README.
|
||||
|
||||
## [2.2.0] - 2023-07-10
|
||||
|
||||
### Added
|
||||
|
||||
- CAPIError class now stores fn_name, error code and message as class attributes.
|
||||
|
||||
### Changed
|
||||
|
||||
- macrobutton capi calls now use error code -9 on AttributeError (using an old version of the API).
|
||||
|
||||
### Fixed
|
||||
|
||||
- call to `self.vm_get_midi_message` now wrapped by {CBindings}.call.
|
||||
|
||||
## [2.1.1] - 2023-07-01
|
||||
|
||||
### Added
|
||||
|
||||
@@ -24,6 +90,10 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- Recorder.loop removed from documentation
|
||||
|
||||
### Changed
|
||||
|
||||
- When out of bounds values are passed, log warnings instead of raising Errors. See [Issue #6][Issue 6].
|
||||
|
||||
## [2.0.0] - 2023-06-25
|
||||
|
||||
Where possible I've attempted to make the changes backwards compatible. The breaking changes affect two higher classes, Strip and Bus, as well as the behaviour of events. All other changes are additive or QOL aimed at giving more options to the developer. For example, every low-level CAPI call is now logged and error raised on Exception, you can now register callback functions as well as observer classes, extra examples to demonstrate different use cases etc.
|
||||
@@ -376,3 +446,4 @@ I will move this commit to a separate branch in preparation for version 2.0.
|
||||
- project packaged with poetry and added to pypi.
|
||||
|
||||
[issue 4]: https://github.com/onyx-and-iris/voicemeeter-api-python/issues/4
|
||||
[Issue 6]: https://github.com/onyx-and-iris/voicemeeter-api-python/issues/6
|
||||
|
||||
86
README.md
86
README.md
@@ -1,7 +1,9 @@
|
||||
[](https://badge.fury.io/py/voicemeeter-api)
|
||||
[](https://github.com/onyx-and-iris/voicemeeter-api-python/blob/dev/LICENSE)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://pycqa.github.io/isort/)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||

|
||||

|
||||
@@ -14,9 +16,9 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## Tested against
|
||||
|
||||
- Basic 1.0.8.8
|
||||
- Banana 2.0.6.8
|
||||
- Potato 3.0.2.8
|
||||
- Basic 1.1.1.1
|
||||
- Banana 2.1.1.1
|
||||
- Potato 3.1.1.1
|
||||
|
||||
## Requirements
|
||||
|
||||
@@ -405,7 +407,7 @@ The following methods are available
|
||||
The following properties are available
|
||||
|
||||
- `A1 - A5`: boolean
|
||||
- `B1 - A3`: boolean
|
||||
- `B1 - B3`: boolean
|
||||
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
|
||||
- `bitresolution`: int, (8, 16, 24, 32)
|
||||
- `channel`: int, from 1 to 8
|
||||
@@ -629,7 +631,9 @@ vm.option.sr = 48000
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `buffer(driver, buffer)` : Set buffer size for particular audio driver.
|
||||
- `buffer(driver, buf)` : Set buffer size for particular audio driver.
|
||||
- buf: int, from 128 to 2048
|
||||
- driver:str, ("mme", "wdm", "ks", "asio")
|
||||
|
||||
example:
|
||||
|
||||
@@ -637,10 +641,6 @@ example:
|
||||
vm.option.buffer("wdm", 512)
|
||||
```
|
||||
|
||||
driver defined as one of ("mme", "wdm", "ks", "asio")
|
||||
|
||||
buffer, from 128 to 2048
|
||||
|
||||
##### delay[i]
|
||||
|
||||
- `get()`: int
|
||||
@@ -693,8 +693,8 @@ vm.apply(
|
||||
Or for each class you may do:
|
||||
|
||||
```python
|
||||
vm.strip[0].apply(mute: True, gain: 3.2, A1: True)
|
||||
vm.vban.outstream[0].apply(on: True, name: 'streamname', bit: 24)
|
||||
vm.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||
vm.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||
```
|
||||
|
||||
## Config Files
|
||||
@@ -703,7 +703,7 @@ vm.vban.outstream[0].apply(on: True, name: 'streamname', bit: 24)
|
||||
|
||||
You may load config files in TOML format.
|
||||
Three example configs have been included with the package. Remember to save
|
||||
current settings before loading a user config. To set one you may do:
|
||||
current settings before loading a user config. To load one you may do:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
@@ -711,7 +711,26 @@ with voicemeeterlib.api('banana') as vm:
|
||||
vm.apply_config('example')
|
||||
```
|
||||
|
||||
will load a user config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||
Your configs may be located in one of the following paths:
|
||||
- \<current working directory\> / "configs" / kind_id
|
||||
- \<user home directory\> / ".config" / "voicemeeter" / kind_id
|
||||
- \<user home directory\> / "Documents" / "Voicemeeter" / "configs" / kind_id
|
||||
|
||||
If a config with the same name is located in multiple locations, only the first one found is loaded into memory, in the above order.
|
||||
|
||||
#### `config extends`
|
||||
|
||||
You may also load a config that extends another config with overrides or additional parameters.
|
||||
|
||||
You just need to define a key `extends` in the config TOML, that names the config to be extended.
|
||||
|
||||
Three example 'extender' configs are included with the repo. You may load them with:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
with voicemeeterlib.api('banana') as vm:
|
||||
vm.apply_config('extender')
|
||||
```
|
||||
|
||||
## Events
|
||||
|
||||
@@ -723,7 +742,7 @@ example:
|
||||
import voicemeeterlib
|
||||
# Set event updates to occur every 50ms
|
||||
# Listen for level updates only
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, ldirty=True}) as vm:
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, ldirty=True) as vm:
|
||||
...
|
||||
```
|
||||
|
||||
@@ -794,6 +813,8 @@ You may pass the following optional keyword arguments:
|
||||
- `mdirty`: boolean=False, macrobutton updates
|
||||
- `midi`: boolean=False, midi updates
|
||||
- `ldirty`: boolean=False, level updates
|
||||
- `timeout`: float=2.0, maximum time to wait for a successful login in seconds
|
||||
- `bits`: int=64, (may be one of 32 or 64), overrides the type of Voicemeeter GUI {Remote}.run_voicemeeter() will launch
|
||||
|
||||
Access to lower level Getters and Setters are provided with these functions:
|
||||
|
||||
@@ -810,19 +831,45 @@ vm.set('Strip[0].Gain', -3.6)
|
||||
|
||||
Access to lower level polling functions are provided with the following property objects:
|
||||
|
||||
#### `vm.pdirty`
|
||||
##### `vm.pdirty`
|
||||
|
||||
True iff a parameter has been updated.
|
||||
|
||||
#### `vm.mdirty`
|
||||
##### `vm.mdirty`
|
||||
|
||||
True iff a macrobutton has been updated.
|
||||
|
||||
#### `vm.ldirty`
|
||||
##### `vm.ldirty`
|
||||
|
||||
True iff a level has been updated.
|
||||
|
||||
|
||||
### Errors
|
||||
|
||||
- `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.
|
||||
- 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
|
||||
|
||||
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||
|
||||
example:
|
||||
```python
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
with voicemeeterlib.api("banana") as vm:
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
### Run tests
|
||||
|
||||
To run all tests:
|
||||
@@ -833,4 +880,7 @@ pytest -v
|
||||
|
||||
### Official Documentation
|
||||
|
||||
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/update-docs/VoicemeeterRemoteAPI.pdf)
|
||||
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemoteAPI.pdf)
|
||||
|
||||
|
||||
[Voicemeeter Remote Header]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemote.h
|
||||
12
configs/banana/extender.toml
Normal file
12
configs/banana/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
12
configs/basic/extender.toml
Normal file
12
configs/basic/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
12
configs/potato/extender.toml
Normal file
12
configs/potato/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
extends = "example"
|
||||
[strip-0]
|
||||
label = "strip0_extended"
|
||||
A1 = false
|
||||
gain = 0.0
|
||||
|
||||
[bus-0]
|
||||
label = "bus0_extended"
|
||||
mute = false
|
||||
|
||||
[vban-in-3]
|
||||
name = "vban_extended"
|
||||
@@ -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,32 +105,67 @@ 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])]
|
||||
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))
|
||||
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))
|
||||
|
||||
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"):
|
||||
if not cmd:
|
||||
break
|
||||
if res := parser.parse((cmd,)):
|
||||
print(res)
|
||||
|
||||
@@ -72,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,7 +1,5 @@
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from logging import config
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
@@ -18,6 +16,7 @@ class App:
|
||||
|
||||
def __enter__(self):
|
||||
self.vm.init_thread()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.vm.end_thread()
|
||||
|
||||
@@ -14,19 +14,20 @@ class App:
|
||||
self.vm.observer.add(self.on_midi)
|
||||
|
||||
def on_midi(self):
|
||||
self.get_info()
|
||||
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"
|
||||
|
||||
231
poetry.lock
generated
231
poetry.lock
generated
@@ -1,21 +1,48 @@
|
||||
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.12.0"
|
||||
version = "24.3.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395"},
|
||||
{file = "black-24.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995"},
|
||||
{file = "black-24.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7"},
|
||||
{file = "black-24.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9"},
|
||||
{file = "black-24.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597"},
|
||||
{file = "black-24.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d"},
|
||||
{file = "black-24.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f"},
|
||||
{file = "black-24.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11"},
|
||||
{file = "black-24.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4"},
|
||||
{file = "black-24.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837"},
|
||||
{file = "black-24.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd"},
|
||||
{file = "black-24.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213"},
|
||||
{file = "black-24.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb"},
|
||||
{file = "black-24.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7"},
|
||||
{file = "black-24.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7"},
|
||||
{file = "black-24.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f"},
|
||||
{file = "black-24.3.0-py3-none-any.whl", hash = "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93"},
|
||||
{file = "black-24.3.0.tar.gz", hash = "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
packaging = ">=22.0"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
@@ -23,25 +50,34 @@ uvloop = ["uvloop (>=0.15.2)"]
|
||||
name = "cachetools"
|
||||
version = "5.3.1"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "cachetools-5.3.1-py3-none-any.whl", hash = "sha256:95ef631eeaea14ba2e36f06437f36463aac3a096799e876ee55e5cdccb102590"},
|
||||
{file = "cachetools-5.3.1.tar.gz", hash = "sha256:dce83f2d9b4e1f732a8cd44af8e8fab2dbe46201467fc98b3ef8f269092bf62b"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.1.0"
|
||||
description = "Universal encoding detector for Python 3"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "chardet-5.1.0-py3-none-any.whl", hash = "sha256:362777fb014af596ad31334fde1e8c327dfdb076e1960d1694662d46a6917ab9"},
|
||||
{file = "chardet-5.1.0.tar.gz", hash = "sha256:0d62712b956bc154f85fb0a266e2a3c5913c2967e00348701b32411d6def31e5"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
@@ -50,25 +86,34 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.6"
|
||||
description = "Distribution utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"},
|
||||
{file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.1.1"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "exceptiongroup-1.1.1-py3-none-any.whl", hash = "sha256:232c37c63e4f682982c8b6459f33a8981039e5fb8756b2074364e5055c498c9e"},
|
||||
{file = "exceptiongroup-1.1.1.tar.gz", hash = "sha256:d484c3090ba2889ae2928419117447a14daf3c1231d5e30d0aae34f354f01785"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
@@ -77,79 +122,103 @@ test = ["pytest (>=6)"]
|
||||
name = "filelock"
|
||||
version = "3.12.2"
|
||||
description = "A platform independent file lock."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "filelock-3.12.2-py3-none-any.whl", hash = "sha256:cbb791cdea2a72f23da6ac5b5269ab0a0d161e9ef0100e653b69049a7706d1ec"},
|
||||
{file = "filelock-3.12.2.tar.gz", hash = "sha256:002740518d8aa59a26b0c76e10fb8c6e15eae825d34b6fdf670333fd7b938d81"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.12.0"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.8.0"
|
||||
files = [
|
||||
{file = "isort-5.12.0-py3-none-any.whl", hash = "sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"},
|
||||
{file = "isort-5.12.0.tar.gz", hash = "sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
colors = ["colorama (>=0.4.3)"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||
plugins = ["setuptools"]
|
||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "1.0.0"
|
||||
description = "Type system extensions for programs checked with the mypy type checker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
files = [
|
||||
{file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
|
||||
{file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "packaging-23.1-py3-none-any.whl", hash = "sha256:994793af429502c4ea2ebf6bf664629d07c1a9fe974af92966e4b8d2df7edc61"},
|
||||
{file = "packaging-23.1.tar.gz", hash = "sha256:a392980d2b6cffa644431898be54b0045151319d1e7ec34f0cfed48767dd334f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.11.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"},
|
||||
{file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "3.6.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "platformdirs-3.6.0-py3-none-any.whl", hash = "sha256:ffa199e3fbab8365778c4a10e1fbf1b9cd50707de826eb304b50e57ec0cc8d38"},
|
||||
{file = "platformdirs-3.6.0.tar.gz", hash = "sha256:57e28820ca8094678b807ff529196506d7a21e17156cb1cddb3e74cebce54640"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.1.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pluggy-1.1.0-py3-none-any.whl", hash = "sha256:d81d19a3a88d82ed06998353ce5d5c02587ef07ee2d808ae63904ab0ccef0087"},
|
||||
{file = "pluggy-1.1.0.tar.gz", hash = "sha256:c500b592c5512df35622e4faf2135aa0b7e989c7d31344194b4afb9d5e47b1bf"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
@@ -159,25 +228,31 @@ testing = ["pytest", "pytest-benchmark"]
|
||||
name = "pyproject-api"
|
||||
version = "1.5.2"
|
||||
description = "API to interact with the python pyproject.toml based projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pyproject_api-1.5.2-py3-none-any.whl", hash = "sha256:9cffcbfb64190f207444d7579d315f3278f2c04ba46d685fad93197b5326d348"},
|
||||
{file = "pyproject_api-1.5.2.tar.gz", hash = "sha256:999f58fa3c92b23ebd31a6bad5d1f87d456744d75e05391be7f5c729015d3d91"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=23.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)"]
|
||||
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.3.2"
|
||||
version = "7.4.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
@@ -194,9 +269,12 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
|
||||
name = "pytest-randomly"
|
||||
version = "3.12.0"
|
||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
|
||||
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = "*"
|
||||
@@ -205,28 +283,63 @@ pytest = "*"
|
||||
name = "pytest-repeat"
|
||||
version = "0.9.1"
|
||||
description = "pytest plugin for repeating tests"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
files = [
|
||||
{file = "pytest-repeat-0.9.1.tar.gz", hash = "sha256:5cd3289745ab3156d43eb9c8e7f7d00a926f3ae5c9cf425bec649b2fe15bad5b"},
|
||||
{file = "pytest_repeat-0.9.1-py2.py3-none-any.whl", hash = "sha256:4474a7d9e9137f6d8cc8ae297f8c4168d33c56dd740aa78cfffe562557e6b96e"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=3.6"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.1.3"
|
||||
description = "An extremely fast Python linter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.1.3-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:b46d43d51f7061652eeadb426a9e3caa1e0002470229ab2fc19de8a7b0766901"},
|
||||
{file = "ruff-0.1.3-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:b8afeb9abd26b4029c72adc9921b8363374f4e7edb78385ffaa80278313a15f9"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca3cf365bf32e9ba7e6db3f48a4d3e2c446cd19ebee04f05338bc3910114528b"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4874c165f96c14a00590dcc727a04dca0cfd110334c24b039458c06cf78a672e"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eec2dd31eed114e48ea42dbffc443e9b7221976554a504767ceaee3dd38edeb8"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:dc3ec4edb3b73f21b4aa51337e16674c752f1d76a4a543af56d7d04e97769613"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e3de9ed2e39160800281848ff4670e1698037ca039bda7b9274f849258d26ce"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c595193881922cc0556a90f3af99b1c5681f0c552e7a2a189956141d8666fe8"},
|
||||
{file = "ruff-0.1.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f75e670d529aa2288cd00fc0e9b9287603d95e1536d7a7e0cafe00f75e0dd9d"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:76dd49f6cd945d82d9d4a9a6622c54a994689d8d7b22fa1322983389b4892e20"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:918b454bc4f8874a616f0d725590277c42949431ceb303950e87fef7a7d94cb3"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8859605e729cd5e53aa38275568dbbdb4fe882d2ea2714c5453b678dca83784"},
|
||||
{file = "ruff-0.1.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:0b6c55f5ef8d9dd05b230bb6ab80bc4381ecb60ae56db0330f660ea240cb0d4a"},
|
||||
{file = "ruff-0.1.3-py3-none-win32.whl", hash = "sha256:3e7afcbdcfbe3399c34e0f6370c30f6e529193c731b885316c5a09c9e4317eef"},
|
||||
{file = "ruff-0.1.3-py3-none-win_amd64.whl", hash = "sha256:7a18df6638cec4a5bd75350639b2bb2a2366e01222825562c7346674bdceb7ea"},
|
||||
{file = "ruff-0.1.3-py3-none-win_arm64.whl", hash = "sha256:12fd53696c83a194a2db7f9a46337ce06445fb9aa7d25ea6f293cf75b21aca9f"},
|
||||
{file = "ruff-0.1.3.tar.gz", hash = "sha256:3ba6145369a151401d5db79f0a47d50e470384d0d89d0d6f7fab0b589ad07c34"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.6.3"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "tox-4.6.3-py3-none-any.whl", hash = "sha256:2946a0bb38924c3a9f9575c7fb4ca1f4c11a7c69c61592f176778892155cb50c"},
|
||||
{file = "tox-4.6.3.tar.gz", hash = "sha256:9e2c5091a117d03b583c57c4c40aecd068099c17d40520e7b165e85c19334534"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=5.3.1"
|
||||
@@ -241,16 +354,30 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
virtualenv = ">=20.23.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx (>=7.0.1)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest (>=7.3.2)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.10.0"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"},
|
||||
{file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.23.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "virtualenv-20.23.1-py3-none-any.whl", hash = "sha256:34da10f14fea9be20e0fd7f04aba9732f84e593dac291b757ce42e3368a39419"},
|
||||
{file = "virtualenv-20.23.1.tar.gz", hash = "sha256:8ff19a38c1021c742148edc4f81cb43d7f8c6816d2ede2ab72af5b84c749ade1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.6,<1"
|
||||
@@ -258,34 +385,10 @@ filelock = ">=3.12,<4"
|
||||
platformdirs = ">=3.5.1,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx (>=7.0.1)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.3.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
||||
|
||||
[metadata.files]
|
||||
black = []
|
||||
cachetools = []
|
||||
chardet = []
|
||||
click = []
|
||||
colorama = []
|
||||
distlib = []
|
||||
exceptiongroup = []
|
||||
filelock = []
|
||||
iniconfig = []
|
||||
isort = []
|
||||
mypy-extensions = []
|
||||
packaging = []
|
||||
pathspec = []
|
||||
platformdirs = []
|
||||
pluggy = []
|
||||
pyproject-api = []
|
||||
pytest = []
|
||||
pytest-randomly = []
|
||||
pytest-repeat = []
|
||||
tomli = []
|
||||
tox = []
|
||||
virtualenv = []
|
||||
content-hash = "d4193db5ebc240288a21da72fa78788dfb71c08b6ecd25e365ce7fb1b0f27ac4"
|
||||
|
||||
114
pyproject.toml
114
pyproject.toml
@@ -1,27 +1,26 @@
|
||||
[tool.poetry]
|
||||
name = "voicemeeter-api"
|
||||
version = "2.0.2"
|
||||
version = "2.6.0"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/voicemeeter-api-python"
|
||||
|
||||
packages = [
|
||||
{ include = "voicemeeterlib" },
|
||||
]
|
||||
packages = [{ include = "voicemeeterlib" }]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.10"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.4"
|
||||
pytest-randomly = "^3.12.0"
|
||||
pytest-repeat = "^0.9.1"
|
||||
black = "^22.3.0"
|
||||
black = ">=22.3,<25.0"
|
||||
isort = "^5.10.1"
|
||||
tox = "^4.6.3"
|
||||
ruff = "^0.1.3"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
@@ -35,12 +34,15 @@ 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 = """
|
||||
[tox]
|
||||
envlist = py310,py311
|
||||
envlist = py310,py311,py312
|
||||
|
||||
[testenv]
|
||||
allowlist_externals = poetry
|
||||
@@ -48,3 +50,97 @@ commands =
|
||||
poetry install -v
|
||||
poetry run pytest tests/
|
||||
"""
|
||||
|
||||
[tool.black]
|
||||
line-length = 88
|
||||
|
||||
[tool.ruff]
|
||||
select = [
|
||||
"E",
|
||||
"F",
|
||||
]
|
||||
ignore = [
|
||||
"E501",
|
||||
]
|
||||
fixable = [
|
||||
"A",
|
||||
"B",
|
||||
"C",
|
||||
"D",
|
||||
"E",
|
||||
"F",
|
||||
"G",
|
||||
"I",
|
||||
"N",
|
||||
"Q",
|
||||
"S",
|
||||
"T",
|
||||
"W",
|
||||
"ANN",
|
||||
"ARG",
|
||||
"BLE",
|
||||
"COM",
|
||||
"DJ",
|
||||
"DTZ",
|
||||
"EM",
|
||||
"ERA",
|
||||
"EXE",
|
||||
"FBT",
|
||||
"ICN",
|
||||
"INP",
|
||||
"ISC",
|
||||
"NPY",
|
||||
"PD",
|
||||
"PGH",
|
||||
"PIE",
|
||||
"PL",
|
||||
"PT",
|
||||
"PTH",
|
||||
"PYI",
|
||||
"RET",
|
||||
"RSE",
|
||||
"RUF",
|
||||
"SIM",
|
||||
"SLF",
|
||||
"TCH",
|
||||
"TID",
|
||||
"TRY",
|
||||
"UP",
|
||||
"YTT",
|
||||
]
|
||||
unfixable = []
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
line-length = 88
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.per-file-ignores]
|
||||
"__init__.py" = [
|
||||
"E402",
|
||||
"F401",
|
||||
]
|
||||
|
||||
48
scripts.py
48
scripts.py
@@ -1,41 +1,59 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_dsl():
|
||||
path = Path.cwd() / "examples" / "dsl" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "dsl" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_events():
|
||||
path = Path.cwd() / "examples" / "events" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "events" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_gui():
|
||||
path = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_levels():
|
||||
path = Path.cwd() / "examples" / "levels" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "levels" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_midi():
|
||||
path = Path.cwd() / "examples" / "midi" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "midi" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
path = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
path = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
scriptpath = Path.cwd() / "examples" / "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"
|
||||
|
||||
@("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")
|
||||
@@ -22,13 +22,13 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 3
|
||||
assert len(vm.bus) == 2
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 4 and len(vm.vban.outstream) == 4
|
||||
assert len(vm.vban.instream) == 6 and len(vm.vban.outstream) == 5
|
||||
|
||||
@pytest.mark.skipif(
|
||||
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")
|
||||
@@ -42,13 +42,13 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 5
|
||||
assert len(vm.bus) == 5
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 8 and len(vm.vban.outstream) == 8
|
||||
assert len(vm.vban.instream) == 10 and len(vm.vban.outstream) == 9
|
||||
|
||||
@pytest.mark.skipif(
|
||||
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")
|
||||
@@ -63,4 +63,4 @@ class TestRemoteFactories:
|
||||
assert len(vm.strip) == 8
|
||||
assert len(vm.bus) == 8
|
||||
assert len(vm.button) == 80
|
||||
assert len(vm.vban.instream) == 8 and len(vm.vban.outstream) == 8
|
||||
assert len(vm.vban.instream) == 10 and len(vm.vban.outstream) == 9
|
||||
|
||||
@@ -173,7 +173,7 @@ class BusDevice(IRemote):
|
||||
|
||||
class VirtualBus(Bus):
|
||||
@classmethod
|
||||
def make(cls, kind):
|
||||
def make(cls, remote, i, kind):
|
||||
"""
|
||||
Factory method for VirtualBus.
|
||||
|
||||
@@ -183,7 +183,13 @@ class VirtualBus(Bus):
|
||||
"""
|
||||
kls = (cls,)
|
||||
if kind.name == "basic":
|
||||
kls += (PhysicalBus,)
|
||||
return type(
|
||||
"VirtualBus",
|
||||
kls,
|
||||
{
|
||||
"device": BusDevice.make(remote, i),
|
||||
},
|
||||
)
|
||||
elif kind.name == "potato":
|
||||
EFFECTS_cls = _make_effects_mixin()
|
||||
kls += (EFFECTS_cls,)
|
||||
@@ -210,7 +216,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 +238,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
|
||||
@@ -306,7 +312,7 @@ def bus_factory(is_phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
BUS_cls = (
|
||||
PhysicalBus.make(remote, i, remote.kind)
|
||||
if is_phys_bus
|
||||
else VirtualBus.make(remote.kind)
|
||||
else VirtualBus.make(remote, i, remote.kind)
|
||||
)
|
||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||
return type(
|
||||
|
||||
@@ -16,109 +16,109 @@ class CBindings(metaclass=ABCMeta):
|
||||
Maps expected ctype argument and res types for each binding.
|
||||
"""
|
||||
|
||||
logger_cbindings = logger.getChild("Cbindings")
|
||||
logger_cbindings = logger.getChild("CBindings")
|
||||
|
||||
vm_login = libc.VBVMR_Login
|
||||
vm_login.restype = LONG
|
||||
vm_login.argtypes = None
|
||||
bind_login = libc.VBVMR_Login
|
||||
bind_login.restype = LONG
|
||||
bind_login.argtypes = None
|
||||
|
||||
vm_logout = libc.VBVMR_Logout
|
||||
vm_logout.restype = LONG
|
||||
vm_logout.argtypes = None
|
||||
bind_logout = libc.VBVMR_Logout
|
||||
bind_logout.restype = LONG
|
||||
bind_logout.argtypes = None
|
||||
|
||||
vm_runvm = libc.VBVMR_RunVoicemeeter
|
||||
vm_runvm.restype = LONG
|
||||
vm_runvm.argtypes = [LONG]
|
||||
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter
|
||||
bind_run_voicemeeter.restype = LONG
|
||||
bind_run_voicemeeter.argtypes = [LONG]
|
||||
|
||||
vm_get_type = libc.VBVMR_GetVoicemeeterType
|
||||
vm_get_type.restype = LONG
|
||||
vm_get_type.argtypes = [ct.POINTER(LONG)]
|
||||
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType
|
||||
bind_get_voicemeeter_type.restype = LONG
|
||||
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
vm_get_version = libc.VBVMR_GetVoicemeeterVersion
|
||||
vm_get_version.restype = LONG
|
||||
vm_get_version.argtypes = [ct.POINTER(LONG)]
|
||||
bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
|
||||
bind_get_voicemeeter_version.restype = LONG
|
||||
bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_IsDirty"):
|
||||
vm_mdirty = libc.VBVMR_MacroButton_IsDirty
|
||||
vm_mdirty.restype = LONG
|
||||
vm_mdirty.argtypes = None
|
||||
bind_macro_button_is_dirty = libc.VBVMR_MacroButton_IsDirty
|
||||
bind_macro_button_is_dirty.restype = LONG
|
||||
bind_macro_button_is_dirty.argtypes = None
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_GetStatus"):
|
||||
vm_get_buttonstatus = libc.VBVMR_MacroButton_GetStatus
|
||||
vm_get_buttonstatus.restype = LONG
|
||||
vm_get_buttonstatus.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||
bind_macro_button_get_status = libc.VBVMR_MacroButton_GetStatus
|
||||
bind_macro_button_get_status.restype = LONG
|
||||
bind_macro_button_get_status.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||
|
||||
if hasattr(libc, "VBVMR_MacroButton_SetStatus"):
|
||||
vm_set_buttonstatus = libc.VBVMR_MacroButton_SetStatus
|
||||
vm_set_buttonstatus.restype = LONG
|
||||
vm_set_buttonstatus.argtypes = [LONG, FLOAT, LONG]
|
||||
bind_macro_button_set_status = libc.VBVMR_MacroButton_SetStatus
|
||||
bind_macro_button_set_status.restype = LONG
|
||||
bind_macro_button_set_status.argtypes = [LONG, FLOAT, LONG]
|
||||
|
||||
vm_pdirty = libc.VBVMR_IsParametersDirty
|
||||
vm_pdirty.restype = LONG
|
||||
vm_pdirty.argtypes = None
|
||||
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
|
||||
bind_is_parameters_dirty.restype = LONG
|
||||
bind_is_parameters_dirty.argtypes = None
|
||||
|
||||
vm_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||
vm_get_parameter_float.restype = LONG
|
||||
vm_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||
bind_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||
bind_get_parameter_float.restype = LONG
|
||||
bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||
|
||||
vm_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||
vm_set_parameter_float.restype = LONG
|
||||
vm_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||
bind_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||
bind_set_parameter_float.restype = LONG
|
||||
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||
|
||||
vm_get_parameter_string = libc.VBVMR_GetParameterStringW
|
||||
vm_get_parameter_string.restype = LONG
|
||||
vm_get_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||
bind_get_parameter_string_w = libc.VBVMR_GetParameterStringW
|
||||
bind_get_parameter_string_w.restype = LONG
|
||||
bind_get_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||
|
||||
vm_set_parameter_string = libc.VBVMR_SetParameterStringW
|
||||
vm_set_parameter_string.restype = LONG
|
||||
vm_set_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||
bind_set_parameter_string_w = libc.VBVMR_SetParameterStringW
|
||||
bind_set_parameter_string_w.restype = LONG
|
||||
bind_set_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||
|
||||
vm_set_parameter_multi = libc.VBVMR_SetParameters
|
||||
vm_set_parameter_multi.restype = LONG
|
||||
vm_set_parameter_multi.argtypes = [ct.POINTER(CHAR)]
|
||||
bind_set_parameters = libc.VBVMR_SetParameters
|
||||
bind_set_parameters.restype = LONG
|
||||
bind_set_parameters.argtypes = [ct.POINTER(CHAR)]
|
||||
|
||||
vm_get_level = libc.VBVMR_GetLevel
|
||||
vm_get_level.restype = LONG
|
||||
vm_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||
bind_get_level = libc.VBVMR_GetLevel
|
||||
bind_get_level.restype = LONG
|
||||
bind_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||
|
||||
vm_get_num_indevices = libc.VBVMR_Input_GetDeviceNumber
|
||||
vm_get_num_indevices.restype = LONG
|
||||
vm_get_num_indevices.argtypes = None
|
||||
bind_input_get_device_number = libc.VBVMR_Input_GetDeviceNumber
|
||||
bind_input_get_device_number.restype = LONG
|
||||
bind_input_get_device_number.argtypes = None
|
||||
|
||||
vm_get_desc_indevices = libc.VBVMR_Input_GetDeviceDescW
|
||||
vm_get_desc_indevices.restype = LONG
|
||||
vm_get_desc_indevices.argtypes = [
|
||||
bind_input_get_device_desc_w = libc.VBVMR_Input_GetDeviceDescW
|
||||
bind_input_get_device_desc_w.restype = LONG
|
||||
bind_input_get_device_desc_w.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
vm_get_num_outdevices = libc.VBVMR_Output_GetDeviceNumber
|
||||
vm_get_num_outdevices.restype = LONG
|
||||
vm_get_num_outdevices.argtypes = None
|
||||
bind_output_get_device_number = libc.VBVMR_Output_GetDeviceNumber
|
||||
bind_output_get_device_number.restype = LONG
|
||||
bind_output_get_device_number.argtypes = None
|
||||
|
||||
vm_get_desc_outdevices = libc.VBVMR_Output_GetDeviceDescW
|
||||
vm_get_desc_outdevices.restype = LONG
|
||||
vm_get_desc_outdevices.argtypes = [
|
||||
bind_output_get_device_desc_w = libc.VBVMR_Output_GetDeviceDescW
|
||||
bind_output_get_device_desc_w.restype = LONG
|
||||
bind_output_get_device_desc_w.argtypes = [
|
||||
LONG,
|
||||
ct.POINTER(LONG),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
ct.POINTER(WCHAR * 256),
|
||||
]
|
||||
|
||||
vm_get_midi_message = libc.VBVMR_GetMidiMessage
|
||||
vm_get_midi_message.restype = LONG
|
||||
vm_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
||||
bind_get_midi_message = libc.VBVMR_GetMidiMessage
|
||||
bind_get_midi_message.restype = LONG
|
||||
bind_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
||||
|
||||
def call(self, func, *args, ok=(0,), ok_exp=None):
|
||||
try:
|
||||
res = func(*args)
|
||||
if ok_exp is None:
|
||||
if res not in ok:
|
||||
raise CAPIError(f"{func.__name__} returned {res}")
|
||||
elif not ok_exp(res):
|
||||
raise CAPIError(f"{func.__name__} returned {res}")
|
||||
raise CAPIError(func.__name__, res)
|
||||
elif not ok_exp(res) and res not in ok:
|
||||
raise CAPIError(func.__name__, res)
|
||||
return res
|
||||
except CAPIError as e:
|
||||
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
||||
|
||||
@@ -147,8 +147,13 @@ class Loader(metaclass=SingletonType):
|
||||
self.logger.info(
|
||||
f"config file with name {identifier} already in memory, skipping.."
|
||||
)
|
||||
return False
|
||||
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):
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
class InstallError(Exception):
|
||||
class VMError(Exception):
|
||||
"""Base voicemeeterlib exception class."""
|
||||
|
||||
|
||||
class InstallError(VMError):
|
||||
"""Exception raised when installation errors occur"""
|
||||
|
||||
|
||||
class CAPIError(Exception):
|
||||
"""Exception raised when the C-API returns error values"""
|
||||
class CAPIError(VMError):
|
||||
"""Exception raised when the C-API returns an error code"""
|
||||
|
||||
|
||||
class VMError(Exception):
|
||||
"""Exception raised when general errors occur"""
|
||||
def __init__(self, fn_name, code):
|
||||
self.fn_name = fn_name
|
||||
self.code = 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)
|
||||
|
||||
@@ -16,7 +16,7 @@ class Event:
|
||||
if self.any():
|
||||
info += (f"now listening for {', '.join(self.get())} events",)
|
||||
else:
|
||||
info += (f"not listening for any events",)
|
||||
info += ("not listening for any events",)
|
||||
self.logger.info(", ".join(info))
|
||||
|
||||
@property
|
||||
|
||||
@@ -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))])
|
||||
@@ -114,6 +114,8 @@ class FactoryBase(Remote):
|
||||
"mdirty": False,
|
||||
"midi": False,
|
||||
"ldirty": False,
|
||||
"timeout": 2,
|
||||
"bits": 64,
|
||||
}
|
||||
if "subs" in kwargs:
|
||||
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
||||
|
||||
@@ -5,36 +5,41 @@ from pathlib import Path
|
||||
|
||||
from .error import InstallError
|
||||
|
||||
bits = 64 if ct.sizeof(ct.c_voidp) == 8 else 32
|
||||
BITS = 64 if ct.sizeof(ct.c_void_p) == 8 else 32
|
||||
|
||||
if platform.system() != "Windows":
|
||||
raise InstallError("Only Windows OS supported")
|
||||
|
||||
|
||||
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
||||
REG_KEY = "".join(
|
||||
[
|
||||
REG_KEY = "\\".join(
|
||||
filter(
|
||||
None,
|
||||
(
|
||||
"SOFTWARE",
|
||||
("\\WOW6432Node" if bits == 64 else ""),
|
||||
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
||||
]
|
||||
"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]
|
||||
return winreg.QueryValueEx(vm_key, r"UninstallString")[0].strip('"')
|
||||
|
||||
|
||||
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
|
||||
raise InstallError("Unable to fetch DLL path from the registry") from e
|
||||
|
||||
DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll'
|
||||
DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll'
|
||||
|
||||
dll_path = vm_parent.joinpath(DLL_NAME)
|
||||
if not dll_path.is_file():
|
||||
|
||||
@@ -30,31 +30,40 @@ class KindMapClass(metaclass=SingletonType):
|
||||
vban: tuple
|
||||
asio: tuple
|
||||
insert: int
|
||||
composite: int
|
||||
|
||||
@property
|
||||
def phys_in(self):
|
||||
def phys_in(self) -> int:
|
||||
return self.ins[0]
|
||||
|
||||
@property
|
||||
def virt_in(self):
|
||||
def virt_in(self) -> int:
|
||||
return self.ins[-1]
|
||||
|
||||
@property
|
||||
def phys_out(self):
|
||||
def phys_out(self) -> int:
|
||||
return self.outs[0]
|
||||
|
||||
@property
|
||||
def virt_out(self):
|
||||
def virt_out(self) -> int:
|
||||
return self.outs[-1]
|
||||
|
||||
@property
|
||||
def num_strip(self):
|
||||
def num_strip(self) -> int:
|
||||
return sum(self.ins)
|
||||
|
||||
@property
|
||||
def num_bus(self):
|
||||
def num_bus(self) -> int:
|
||||
return sum(self.outs)
|
||||
|
||||
@property
|
||||
def num_strip_levels(self) -> int:
|
||||
return 2 * self.phys_in + 8 * self.virt_in
|
||||
|
||||
@property
|
||||
def num_bus_levels(self) -> int:
|
||||
return 8 * (self.phys_out + self.virt_out)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.capitalize()
|
||||
|
||||
@@ -64,9 +73,10 @@ class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4)
|
||||
vban: tuple = (4, 4, 1, 1)
|
||||
asio: tuple = (0, 0)
|
||||
insert: int = 0
|
||||
composite: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -74,9 +84,10 @@ class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
asio: tuple = (6, 8)
|
||||
insert: int = 22
|
||||
composite: int = 8
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -84,9 +95,10 @@ class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
asio: tuple = (10, 8)
|
||||
insert: int = 34
|
||||
composite: int = 8
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
from enum import IntEnum
|
||||
|
||||
from .iremote import IRemote
|
||||
|
||||
ButtonModes = IntEnum(
|
||||
"ButtonModes",
|
||||
"state stateonly trigger",
|
||||
start=1,
|
||||
)
|
||||
|
||||
|
||||
class Adapter(IRemote):
|
||||
"""Adapter to the common interface."""
|
||||
@@ -8,9 +16,13 @@ class Adapter(IRemote):
|
||||
pass
|
||||
|
||||
def getter(self, mode):
|
||||
self.logger.debug(f"getter: button[{self.index}].{ButtonModes(mode).name}")
|
||||
return self._remote.get_buttonstatus(self.index, mode)
|
||||
|
||||
def setter(self, val, mode):
|
||||
def setter(self, mode, val):
|
||||
self.logger.debug(
|
||||
f"setter: button[{self.index}].{ButtonModes(mode).name}={val}"
|
||||
)
|
||||
self._remote.set_buttonstatus(self.index, val, mode)
|
||||
|
||||
|
||||
@@ -22,24 +34,24 @@ class MacroButton(Adapter):
|
||||
|
||||
@property
|
||||
def state(self) -> bool:
|
||||
return self.getter(1) == 1
|
||||
return self.getter(ButtonModes.state) == 1
|
||||
|
||||
@state.setter
|
||||
def state(self, val):
|
||||
self.setter(1 if val else 0, 1)
|
||||
def state(self, val: bool):
|
||||
self.setter(ButtonModes.state, 1 if val else 0)
|
||||
|
||||
@property
|
||||
def stateonly(self) -> bool:
|
||||
return self.getter(2) == 1
|
||||
return self.getter(ButtonModes.stateonly) == 1
|
||||
|
||||
@stateonly.setter
|
||||
def stateonly(self, val):
|
||||
self.setter(1 if val else 0, 2)
|
||||
def stateonly(self, val: bool):
|
||||
self.setter(ButtonModes.stateonly, 1 if val else 0)
|
||||
|
||||
@property
|
||||
def trigger(self) -> bool:
|
||||
return self.getter(3) == 1
|
||||
return self.getter(ButtonModes.trigger) == 1
|
||||
|
||||
@trigger.setter
|
||||
def trigger(self, val):
|
||||
self.setter(1 if val else 0, 3)
|
||||
def trigger(self, val: bool):
|
||||
self.setter(ButtonModes.trigger, 1 if val else 0)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from typing import Optional
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
@@ -71,7 +70,7 @@ class Patch(IRemote):
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"patch"
|
||||
return "patch"
|
||||
|
||||
@property
|
||||
def postfadercomp(self) -> bool:
|
||||
@@ -93,7 +92,7 @@ class Patch(IRemote):
|
||||
class Asio(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"patch"
|
||||
return "patch"
|
||||
|
||||
|
||||
class AsioIn(Asio):
|
||||
@@ -196,7 +195,7 @@ class Option(IRemote):
|
||||
def sr(self, val: int):
|
||||
opts = (44100, 48000, 88200, 96000, 176400, 192000)
|
||||
if val not in opts:
|
||||
raise VMError(f"Expected one of: {opts}")
|
||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
|
||||
@@ -126,7 +126,7 @@ class Recorder(IRemote):
|
||||
|
||||
time_str = str(time_str) # coerce the type
|
||||
if (
|
||||
match := re.match(
|
||||
re.match(
|
||||
r"^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$",
|
||||
time_str,
|
||||
)
|
||||
@@ -135,7 +135,7 @@ class Recorder(IRemote):
|
||||
self.setter("goto", get_sec())
|
||||
else:
|
||||
self.logger.warning(
|
||||
f"goto expects a string that matches the format 'hh:mm:ss'"
|
||||
"goto expects a string that matches the format 'hh:mm:ss'"
|
||||
)
|
||||
|
||||
def filetype(self, val: str):
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
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
|
||||
from .event import Event
|
||||
from .inst import bits
|
||||
from .inst import BITS
|
||||
from .kinds import KindId
|
||||
from .misc import Midi, VmGui
|
||||
from .subject import Subject
|
||||
from .updater import Producer, Updater
|
||||
from .util import grouper, polling, script
|
||||
from .util import deep_merge, grouper, polling, script, timeout
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -28,16 +29,22 @@ 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():
|
||||
setattr(self, attr, val)
|
||||
|
||||
if self.bits not in (32, 64):
|
||||
self.logger.warning(
|
||||
f"kwarg bits got {self.bits}, expected either 32 or 64, defaulting to 64"
|
||||
)
|
||||
self.bits = 64
|
||||
|
||||
def __enter__(self):
|
||||
"""setup procedures"""
|
||||
self.login()
|
||||
@@ -52,49 +59,50 @@ 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()
|
||||
|
||||
@timeout
|
||||
def login(self) -> None:
|
||||
"""Login to the API, initialize dirty parameters"""
|
||||
self.gui.launched = self.call(self.vm_login, ok=(0, 1)) == 0
|
||||
self.gui.launched = self.call(self.bind_login, ok=(0, 1)) == 0
|
||||
if not self.gui.launched:
|
||||
self.logger.info(
|
||||
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
|
||||
)
|
||||
self.run_voicemeeter(self.kind.name)
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
||||
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:
|
||||
value = KindId[kind_id.upper()].value + 3
|
||||
else:
|
||||
value = KindId[kind_id.upper()].value
|
||||
self.call(self.vm_runvm, value)
|
||||
time.sleep(1)
|
||||
if BITS == 64 and self.bits == 64:
|
||||
value += 3
|
||||
self.call(self.bind_run_voicemeeter, value)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
|
||||
type_ = ct.c_long()
|
||||
self.call(self.vm_get_type, ct.byref(type_))
|
||||
self.call(self.bind_get_voicemeeter_type, ct.byref(type_))
|
||||
return KindId(type_.value).name.lower()
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
ver = ct.c_long()
|
||||
self.call(self.vm_get_version, ct.byref(ver))
|
||||
self.call(self.bind_get_voicemeeter_version, ct.byref(ver))
|
||||
return "{}.{}.{}.{}".format(
|
||||
(ver.value & 0xFF000000) >> 24,
|
||||
(ver.value & 0x00FF0000) >> 16,
|
||||
@@ -105,18 +113,16 @@ class Remote(CBindings):
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
"""True iff UI parameters have been updated."""
|
||||
return self.call(self.vm_pdirty, ok=(0, 1)) == 1
|
||||
return self.call(self.bind_is_parameters_dirty, ok=(0, 1)) == 1
|
||||
|
||||
@property
|
||||
def mdirty(self) -> bool:
|
||||
"""True iff MB parameters have been updated."""
|
||||
try:
|
||||
return self.call(self.vm_mdirty, ok=(0, 1)) == 1
|
||||
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}")
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_IsDirty. are you using an old version of the API?"
|
||||
) from e
|
||||
raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
@@ -127,12 +133,14 @@ 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
|
||||
except CAPIError:
|
||||
self.logger.error("no bind for mdirty, clearing pdirty only")
|
||||
except CAPIError as e:
|
||||
if not (e.fn_name == "VBVMR_MacroButton_IsDirty" and e.code == -9):
|
||||
raise
|
||||
self.logger.error(f"{e} clearing pdirty only.")
|
||||
while self.pdirty:
|
||||
pass
|
||||
|
||||
@@ -141,59 +149,62 @@ class Remote(CBindings):
|
||||
"""Gets a string or float parameter"""
|
||||
if is_string:
|
||||
buf = ct.create_unicode_buffer(512)
|
||||
self.call(self.vm_get_parameter_string, param.encode(), ct.byref(buf))
|
||||
self.call(self.bind_get_parameter_string_w, param.encode(), ct.byref(buf))
|
||||
else:
|
||||
buf = ct.c_float()
|
||||
self.call(self.vm_get_parameter_float, param.encode(), ct.byref(buf))
|
||||
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:
|
||||
raise VMError("String is too long")
|
||||
self.call(self.vm_set_parameter_string, param.encode(), ct.c_wchar_p(val))
|
||||
self.call(
|
||||
self.bind_set_parameter_string_w, param.encode(), ct.c_wchar_p(val)
|
||||
)
|
||||
else:
|
||||
self.call(
|
||||
self.vm_set_parameter_float, param.encode(), ct.c_float(float(val))
|
||||
self.bind_set_parameter_float, param.encode(), ct.c_float(float(val))
|
||||
)
|
||||
self.cache[param] = val
|
||||
|
||||
@polling
|
||||
def get_buttonstatus(self, id: int, mode: int) -> int:
|
||||
def get_buttonstatus(self, id_: int, mode: int) -> int:
|
||||
"""Gets a macrobutton parameter"""
|
||||
state = ct.c_float()
|
||||
c_state = ct.c_float()
|
||||
try:
|
||||
self.call(
|
||||
self.vm_get_buttonstatus,
|
||||
ct.c_long(id),
|
||||
ct.byref(state),
|
||||
self.bind_macro_button_get_status,
|
||||
ct.c_long(id_),
|
||||
ct.byref(c_state),
|
||||
ct.c_long(mode),
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_GetStatus. are you using an old version of the API?"
|
||||
) from e
|
||||
return int(state.value)
|
||||
raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
|
||||
return int(c_state.value)
|
||||
|
||||
def set_buttonstatus(self, id: int, state: 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(state))
|
||||
c_state = ct.c_float(float(val))
|
||||
try:
|
||||
self.call(self.vm_set_buttonstatus, ct.c_long(id), c_state, ct.c_long(mode))
|
||||
self.call(
|
||||
self.bind_macro_button_set_status,
|
||||
ct.c_long(id_),
|
||||
c_state,
|
||||
ct.c_long(mode),
|
||||
)
|
||||
except AttributeError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise CAPIError(
|
||||
"no bind for VBVMR_MacroButton_SetStatus. are you using an old version of the API?"
|
||||
) from e
|
||||
self.cache[f"mb_{id}_{mode}"] = int(c_state.value)
|
||||
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:
|
||||
"""Retrieves number of physical devices connected"""
|
||||
if direction not in ("in", "out"):
|
||||
raise VMError("Expected a direction: in or out")
|
||||
func = getattr(self, f"vm_get_num_{direction}devices")
|
||||
func = getattr(self, f"bind_{direction}put_get_device_number")
|
||||
res = self.call(func, ok_exp=lambda r: r >= 0)
|
||||
return res
|
||||
|
||||
@@ -204,7 +215,7 @@ class Remote(CBindings):
|
||||
type_ = ct.c_long()
|
||||
name = ct.create_unicode_buffer(256)
|
||||
hwid = ct.create_unicode_buffer(256)
|
||||
func = getattr(self, f"vm_get_desc_{direction}devices")
|
||||
func = getattr(self, f"bind_{direction}put_get_device_desc_w")
|
||||
self.call(
|
||||
func,
|
||||
ct.c_long(index),
|
||||
@@ -217,7 +228,9 @@ class Remote(CBindings):
|
||||
def get_level(self, type_: int, index: int) -> float:
|
||||
"""Retrieves a single level value"""
|
||||
val = ct.c_float()
|
||||
self.call(self.vm_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val))
|
||||
self.call(
|
||||
self.bind_get_level, ct.c_long(type_), ct.c_long(index), ct.byref(val)
|
||||
)
|
||||
return val.value
|
||||
|
||||
def _get_levels(self) -> Iterable:
|
||||
@@ -227,18 +240,21 @@ class Remote(CBindings):
|
||||
return (
|
||||
tuple(
|
||||
self.get_level(self.strip_mode, i)
|
||||
for i in range(2 * self.kind.phys_in + 8 * self.kind.virt_in)
|
||||
),
|
||||
tuple(
|
||||
self.get_level(3, i)
|
||||
for i in range(8 * (self.kind.phys_out + self.kind.virt_out))
|
||||
for i in range(self.kind.num_strip_levels)
|
||||
),
|
||||
tuple(self.get_level(3, i) for i in range(self.kind.num_bus_levels)),
|
||||
)
|
||||
|
||||
def get_midi_message(self):
|
||||
n = ct.c_long(1024)
|
||||
buf = ct.create_string_buffer(1024)
|
||||
res = self.vm_get_midi_message(ct.byref(buf), n, ok_exp=lambda r: r >= 0)
|
||||
res = self.call(
|
||||
self.bind_get_midi_message,
|
||||
ct.byref(buf),
|
||||
n,
|
||||
ok=(-5, -6), # no data received from midi device
|
||||
ok_exp=lambda r: r >= 0,
|
||||
)
|
||||
if res > 0:
|
||||
vals = tuple(
|
||||
grouper(3, (int.from_bytes(buf[i], "little") for i in range(res)))
|
||||
@@ -256,7 +272,7 @@ class Remote(CBindings):
|
||||
"""Sets many parameters from a script"""
|
||||
if len(script) > 48000:
|
||||
raise ValueError("Script too large, max size 48kB")
|
||||
self.call(self.vm_set_parameter_multi, script.encode())
|
||||
self.call(self.bind_set_parameters, script.encode())
|
||||
time.sleep(self.DELAY * 5)
|
||||
|
||||
def apply(self, data: dict):
|
||||
@@ -266,41 +282,64 @@ 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"""
|
||||
error_msg = (
|
||||
ERR_MSG = (
|
||||
f"No config with name '{name}' is loaded into memory",
|
||||
f"Known configs: {list(self.configs.keys())}",
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
except KeyError:
|
||||
self.logger.error(("\n").join(error_msg))
|
||||
config = self.configs[name]
|
||||
except KeyError as e:
|
||||
self.logger.error(("\n").join(ERR_MSG))
|
||||
raise VMError(("\n").join(ERR_MSG)) from e
|
||||
|
||||
def logout(self) -> NoReturn:
|
||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
||||
self.clear_dirty()
|
||||
time.sleep(0.1)
|
||||
self.call(self.vm_logout)
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||
if "extends" in config:
|
||||
extended = config["extends"]
|
||||
config = {
|
||||
k: v
|
||||
for k, v in deep_merge(self.configs[extended], config)
|
||||
if k not in ("extends")
|
||||
}
|
||||
self.logger.debug(
|
||||
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||
)
|
||||
self.apply(config)
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
|
||||
def end_thread(self):
|
||||
if not self.stopped():
|
||||
self.logger.debug("events thread shutdown started")
|
||||
self.running = False
|
||||
self.stop_event.set()
|
||||
self.producer.join() # wait for producer thread to complete cycle
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> 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) -> None:
|
||||
"""teardown procedures"""
|
||||
self.end_thread()
|
||||
self.logout()
|
||||
|
||||
@@ -90,7 +90,7 @@ class PhysicalStrip(Strip):
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
f"PhysicalStrip",
|
||||
"PhysicalStrip",
|
||||
(cls, EFFECTS_cls),
|
||||
{
|
||||
"comp": StripComp(remote, i),
|
||||
@@ -168,7 +168,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def knee(self) -> float:
|
||||
return round(self.getter("Knee"), 1)
|
||||
return round(self.getter("Knee"), 2)
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: float):
|
||||
@@ -337,7 +337,7 @@ class VirtualStrip(Strip):
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
f"VirtualStrip",
|
||||
"VirtualStrip",
|
||||
(cls, EFFECTS_cls),
|
||||
{},
|
||||
)
|
||||
@@ -364,7 +364,7 @@ class VirtualStrip(Strip):
|
||||
self.setter("karaoke", val)
|
||||
|
||||
@property
|
||||
def bass(self):
|
||||
def bass(self) -> float:
|
||||
return round(self.getter("EQGain1"), 1)
|
||||
|
||||
@bass.setter
|
||||
@@ -372,7 +372,7 @@ class VirtualStrip(Strip):
|
||||
self.setter("EQGain1", val)
|
||||
|
||||
@property
|
||||
def mid(self):
|
||||
def mid(self) -> float:
|
||||
return round(self.getter("EQGain2"), 1)
|
||||
|
||||
@mid.setter
|
||||
@@ -382,7 +382,7 @@ class VirtualStrip(Strip):
|
||||
med = mid
|
||||
|
||||
@property
|
||||
def treble(self):
|
||||
def treble(self) -> float:
|
||||
return round(self.getter("EQGain3"), 1)
|
||||
|
||||
high = treble
|
||||
@@ -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
|
||||
@@ -491,7 +491,7 @@ class GainLayer(IRemote):
|
||||
def _make_gainlayer_mixin(remote, index):
|
||||
"""Creates a GainLayer mixin"""
|
||||
return type(
|
||||
f"GainlayerMixin",
|
||||
"GainlayerMixin",
|
||||
(),
|
||||
{
|
||||
"gainlayer": tuple(
|
||||
|
||||
@@ -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:
|
||||
@@ -36,10 +40,8 @@ class Updater(threading.Thread):
|
||||
super().__init__(name="updater", daemon=True)
|
||||
self._remote = remote
|
||||
self.queue = queue
|
||||
self._remote._strip_comp = [False] * (
|
||||
2 * self._remote.kind.phys_in + 8 * self._remote.kind.virt_in
|
||||
)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
||||
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
@@ -58,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:
|
||||
@@ -75,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")
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
import functools
|
||||
import time
|
||||
from itertools import zip_longest
|
||||
from typing import Iterator
|
||||
|
||||
from .error import CAPIError, VMError
|
||||
|
||||
|
||||
def timeout(func):
|
||||
"""
|
||||
Times out the login function once time elapsed exceeds remote.timeout.
|
||||
"""
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
remote, *_ = args
|
||||
func(*args, **kwargs)
|
||||
|
||||
err = None
|
||||
start = time.time()
|
||||
while time.time() < start + remote.timeout:
|
||||
try:
|
||||
time.sleep(0.1) # ensure at least 0.1 delay before clearing dirty
|
||||
remote.logger.info(
|
||||
f"{type(remote).__name__}: Successfully logged into {remote} version {remote.version}"
|
||||
)
|
||||
remote.logger.debug(f"login time: {round(time.time() - start, 2)}")
|
||||
err = None
|
||||
break
|
||||
except CAPIError as e:
|
||||
err = e
|
||||
continue
|
||||
if err:
|
||||
raise VMError("Timeout logging into the api")
|
||||
remote.clear_dirty()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def polling(func):
|
||||
"""
|
||||
@@ -70,3 +104,17 @@ def grouper(n, iterable, fillvalue=None):
|
||||
"""
|
||||
args = [iter(iterable)] * n
|
||||
return zip_longest(fillvalue=fillvalue, *args)
|
||||
|
||||
|
||||
def deep_merge(dict1, dict2):
|
||||
"""Generator function for deep merging two dicts"""
|
||||
for k in set(dict1) | set(dict2):
|
||||
if k in dict1 and k in dict2:
|
||||
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
|
||||
yield k, dict(deep_merge(dict1[k], dict2[k]))
|
||||
else:
|
||||
yield k, dict2[k]
|
||||
elif k in dict1:
|
||||
yield k, dict1[k]
|
||||
else:
|
||||
yield k, dict2[k]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
@@ -50,7 +50,9 @@ class VbanStream(IRemote):
|
||||
@port.setter
|
||||
def port(self, val: int):
|
||||
if not 1024 <= val <= 65535:
|
||||
raise VMError("Expected value from 1024 to 65535")
|
||||
self.logger.warning(
|
||||
f"port got: {val} but expected a value from 1024 to 65535"
|
||||
)
|
||||
self.setter("port", val)
|
||||
|
||||
@property
|
||||
@@ -61,7 +63,7 @@ class VbanStream(IRemote):
|
||||
def sr(self, val: int):
|
||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||
if val not in opts:
|
||||
raise VMError(f"Expected one of: {opts}")
|
||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
@@ -71,7 +73,7 @@ class VbanStream(IRemote):
|
||||
@channel.setter
|
||||
def channel(self, val: int):
|
||||
if not 1 <= val <= 8:
|
||||
raise VMError("Expected value from 1 to 8")
|
||||
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
|
||||
self.setter("channel", val)
|
||||
|
||||
@property
|
||||
@@ -81,7 +83,7 @@ class VbanStream(IRemote):
|
||||
@bit.setter
|
||||
def bit(self, val: int):
|
||||
if val not in (16, 24):
|
||||
raise VMError("Expected value 16 or 24")
|
||||
self.logger.warning(f"bit got: {val} but expected value 16 or 24")
|
||||
self.setter("bit", 1 if (val == 16) else 2)
|
||||
|
||||
@property
|
||||
@@ -91,7 +93,7 @@ class VbanStream(IRemote):
|
||||
@quality.setter
|
||||
def quality(self, val: int):
|
||||
if not 0 <= val <= 4:
|
||||
raise VMError("Expected value from 0 to 4")
|
||||
self.logger.warning(f"quality got: {val} but expected a value from 0 to 4")
|
||||
self.setter("quality", val)
|
||||
|
||||
@property
|
||||
@@ -101,7 +103,7 @@ class VbanStream(IRemote):
|
||||
@route.setter
|
||||
def route(self, val: int):
|
||||
if not 0 <= val <= 8:
|
||||
raise VMError("Expected value from 0 to 8")
|
||||
self.logger.warning(f"route got: {val} but expected a value from 0 to 8")
|
||||
self.setter("route", val)
|
||||
|
||||
|
||||
@@ -132,6 +134,18 @@ class VbanInstream(VbanStream):
|
||||
return super(VbanInstream, self).bit
|
||||
|
||||
|
||||
class VbanAudioInstream(VbanInstream):
|
||||
"""Represents a VBAN Audio Instream"""
|
||||
|
||||
|
||||
class VbanMidiInstream(VbanInstream):
|
||||
"""Represents a VBAN Midi Instream"""
|
||||
|
||||
|
||||
class VbanTextInstream(VbanInstream):
|
||||
"""Represents a VBAN Text Instream"""
|
||||
|
||||
|
||||
class VbanOutstream(VbanStream):
|
||||
"""
|
||||
class representing a vban outstream
|
||||
@@ -147,6 +161,42 @@ class VbanOutstream(VbanStream):
|
||||
return "out"
|
||||
|
||||
|
||||
class VbanAudioOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Audio Outstream"""
|
||||
|
||||
|
||||
class VbanMidiOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Midi Outstream"""
|
||||
|
||||
|
||||
def _make_stream_pair(remote, kind):
|
||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||
|
||||
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(_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)),
|
||||
)
|
||||
|
||||
|
||||
def _make_stream_pairs(remote):
|
||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||
|
||||
|
||||
class Vban:
|
||||
"""
|
||||
class representing the vban module
|
||||
@@ -156,9 +206,7 @@ class Vban:
|
||||
|
||||
def __init__(self, remote):
|
||||
self.remote = remote
|
||||
num_instream, num_outstream = remote.kind.vban
|
||||
self.instream = tuple(VbanInstream(remote, i) for i in range(num_instream))
|
||||
self.outstream = tuple(VbanOutstream(remote, i) for i in range(num_outstream))
|
||||
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||
|
||||
def enable(self):
|
||||
self.remote.set("vban.Enable", 1)
|
||||
|
||||
Reference in New Issue
Block a user