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]
|
- [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
|
### 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
|
- 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
|
## [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.
|
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.
|
- project packaged with poetry and added to pypi.
|
||||||
|
|
||||||
[issue 4]: https://github.com/onyx-and-iris/voicemeeter-api-python/issues/4
|
[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://badge.fury.io/py/voicemeeter-api)
|
||||||
[](https://github.com/onyx-and-iris/voicemeeter-api-python/blob/dev/LICENSE)
|
[](https://github.com/onyx-and-iris/voicemeeter-api-python/blob/dev/LICENSE)
|
||||||
|
[](https://python-poetry.org/)
|
||||||
[](https://github.com/psf/black)
|
[](https://github.com/psf/black)
|
||||||
[](https://pycqa.github.io/isort/)
|
[](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
|
## Tested against
|
||||||
|
|
||||||
- Basic 1.0.8.8
|
- Basic 1.1.1.1
|
||||||
- Banana 2.0.6.8
|
- Banana 2.1.1.1
|
||||||
- Potato 3.0.2.8
|
- Potato 3.1.1.1
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -405,7 +407,7 @@ The following methods are available
|
|||||||
The following properties are available
|
The following properties are available
|
||||||
|
|
||||||
- `A1 - A5`: boolean
|
- `A1 - A5`: boolean
|
||||||
- `B1 - A3`: boolean
|
- `B1 - B3`: boolean
|
||||||
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
|
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
|
||||||
- `bitresolution`: int, (8, 16, 24, 32)
|
- `bitresolution`: int, (8, 16, 24, 32)
|
||||||
- `channel`: int, from 1 to 8
|
- `channel`: int, from 1 to 8
|
||||||
@@ -629,7 +631,9 @@ vm.option.sr = 48000
|
|||||||
|
|
||||||
The following methods are available:
|
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:
|
example:
|
||||||
|
|
||||||
@@ -637,10 +641,6 @@ example:
|
|||||||
vm.option.buffer("wdm", 512)
|
vm.option.buffer("wdm", 512)
|
||||||
```
|
```
|
||||||
|
|
||||||
driver defined as one of ("mme", "wdm", "ks", "asio")
|
|
||||||
|
|
||||||
buffer, from 128 to 2048
|
|
||||||
|
|
||||||
##### delay[i]
|
##### delay[i]
|
||||||
|
|
||||||
- `get()`: int
|
- `get()`: int
|
||||||
@@ -693,8 +693,8 @@ vm.apply(
|
|||||||
Or for each class you may do:
|
Or for each class you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vm.strip[0].apply(mute: True, gain: 3.2, A1: True)
|
vm.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||||
vm.vban.outstream[0].apply(on: True, name: 'streamname', bit: 24)
|
vm.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config Files
|
## 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.
|
You may load config files in TOML format.
|
||||||
Three example configs have been included with the package. Remember to save
|
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
|
```python
|
||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
@@ -711,7 +711,26 @@ with voicemeeterlib.api('banana') as vm:
|
|||||||
vm.apply_config('example')
|
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
|
## Events
|
||||||
|
|
||||||
@@ -723,7 +742,7 @@ example:
|
|||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
# Set event updates to occur every 50ms
|
# Set event updates to occur every 50ms
|
||||||
# Listen for level updates only
|
# 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
|
- `mdirty`: boolean=False, macrobutton updates
|
||||||
- `midi`: boolean=False, midi updates
|
- `midi`: boolean=False, midi updates
|
||||||
- `ldirty`: boolean=False, level 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:
|
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:
|
Access to lower level polling functions are provided with the following property objects:
|
||||||
|
|
||||||
#### `vm.pdirty`
|
##### `vm.pdirty`
|
||||||
|
|
||||||
True iff a parameter has been updated.
|
True iff a parameter has been updated.
|
||||||
|
|
||||||
#### `vm.mdirty`
|
##### `vm.mdirty`
|
||||||
|
|
||||||
True iff a macrobutton has been updated.
|
True iff a macrobutton has been updated.
|
||||||
|
|
||||||
#### `vm.ldirty`
|
##### `vm.ldirty`
|
||||||
|
|
||||||
True iff a level has been updated.
|
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
|
### Run tests
|
||||||
|
|
||||||
To run all tests:
|
To run all tests:
|
||||||
@@ -833,4 +880,7 @@ pytest -v
|
|||||||
|
|
||||||
### Official Documentation
|
### 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 argparse
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
from pyparsing import (
|
from pyparsing import (
|
||||||
Combine,
|
Combine,
|
||||||
@@ -10,24 +12,88 @@ from pyparsing import (
|
|||||||
Suppress,
|
Suppress,
|
||||||
Word,
|
Word,
|
||||||
alphanums,
|
alphanums,
|
||||||
alphas,
|
|
||||||
nums,
|
nums,
|
||||||
)
|
)
|
||||||
|
|
||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
argparser = argparse.ArgumentParser(description="creates a basic dsl")
|
argparser = argparse.ArgumentParser(description="creates a basic dsl")
|
||||||
argparser.add_argument("-i", action="store_true")
|
argparser.add_argument("-i", action="store_true")
|
||||||
args = argparser.parse_args()
|
args = argparser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
ParamKinds = IntEnum(
|
||||||
|
"ParamKinds",
|
||||||
|
"bool float string",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Strategy(ABC):
|
||||||
|
def __init__(self, target, param, val):
|
||||||
|
self.target = target
|
||||||
|
self.param = param
|
||||||
|
self.val = val
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def run(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BoolStrategy(Strategy):
|
||||||
|
def run(self):
|
||||||
|
setattr(self.target, self.param, self.strtobool(self.val))
|
||||||
|
|
||||||
|
def strtobool(self, val):
|
||||||
|
"""Convert a string representation of truth to it's numeric form."""
|
||||||
|
|
||||||
|
val = val.lower()
|
||||||
|
if val in ("y", "yes", "t", "true", "on", "1"):
|
||||||
|
return 1
|
||||||
|
elif val in ("n", "no", "f", "false", "off", "0"):
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
raise ValueError("invalid truth value %r" % (val,))
|
||||||
|
|
||||||
|
|
||||||
|
class FloatStrategy(Strategy):
|
||||||
|
def run(self):
|
||||||
|
setattr(self.target, self.param, float(self.val))
|
||||||
|
|
||||||
|
|
||||||
|
class StringStrategy(Strategy):
|
||||||
|
def run(self):
|
||||||
|
setattr(self.target, self.param, " ".join(self.val))
|
||||||
|
|
||||||
|
|
||||||
|
class Context:
|
||||||
|
def __init__(self, strategy: Strategy) -> None:
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
@property
|
||||||
|
def strategy(self) -> Strategy:
|
||||||
|
return self._strategy
|
||||||
|
|
||||||
|
@strategy.setter
|
||||||
|
def strategy(self, strategy: Strategy) -> None:
|
||||||
|
self._strategy = strategy
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
self.strategy.run()
|
||||||
|
|
||||||
|
|
||||||
class Parser:
|
class Parser:
|
||||||
|
IS_STRING = ("label",)
|
||||||
|
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.vm = vm
|
self.vm = vm
|
||||||
self.kls = Group(OneOrMore(Word(alphanums)))
|
self.kls = Group(OneOrMore(Word(alphanums)))
|
||||||
self.token = Suppress("->")
|
self.token = Suppress("->")
|
||||||
self.param = Word(alphanums)
|
self.param = Group(OneOrMore(Word(alphanums)))
|
||||||
self.value = Combine(
|
self.value = Combine(
|
||||||
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
|
Optional("-") + Word(nums) + Optional(".") + Optional(Word(nums))
|
||||||
) | Group(OneOrMore(Word(alphanums)))
|
) | Group(OneOrMore(Word(alphanums)))
|
||||||
@@ -39,32 +105,67 @@ class Parser:
|
|||||||
+ Optional(self.value)
|
+ Optional(self.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
def parse(self, cmds):
|
def converter(self, cmds):
|
||||||
|
"""determines the kind of parameter from the parsed string"""
|
||||||
|
|
||||||
res = list()
|
res = list()
|
||||||
|
|
||||||
for cmd in cmds:
|
for cmd in cmds:
|
||||||
if len(self.event.parseString(cmd)) == 2:
|
self.logger.debug(f"running command: {cmd}")
|
||||||
kls, param = self.event.parseString(cmd)
|
match cmd_parsed := self.event.parseString(cmd):
|
||||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
case [[kls, index], [param]]:
|
||||||
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
res.append(getattr(target, param))
|
res.append(getattr(target, param))
|
||||||
elif len(self.event.parseString(cmd)) == 3:
|
case [[kls, index], [param], val] if param in self.IS_STRING:
|
||||||
kls, param, val = self.event.parseString(cmd)
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
target = getattr(self.vm, kls[0])[int(kls[-1])]
|
context = self._get_context(ParamKinds.string, target, param, val)
|
||||||
if "".join(val) in ["off", "on"]:
|
context.run()
|
||||||
setattr(target, param, bool(["off", "on"].index("".join(val))))
|
case [[kls, index], [param], [val] | val]:
|
||||||
elif param in ["gain", "comp", "gate", "limit", "audibility"]:
|
target = getattr(self.vm, kls)[int(index)]
|
||||||
setattr(target, param, float("".join(val)))
|
try:
|
||||||
elif param in ["label"]:
|
context = self._get_context(ParamKinds.bool, target, param, val)
|
||||||
setattr(target, param, " ".join(val))
|
context.run()
|
||||||
|
except ValueError as e:
|
||||||
|
self.logger.error(f"{e}... switching to float strategy")
|
||||||
|
context.strategy = FloatStrategy(target, param, val)
|
||||||
|
context.run()
|
||||||
|
case [
|
||||||
|
[kls, index],
|
||||||
|
[secondary, param],
|
||||||
|
[val] | val,
|
||||||
|
]:
|
||||||
|
primary = getattr(self.vm, kls)[int(index)]
|
||||||
|
target = getattr(primary, secondary)
|
||||||
|
try:
|
||||||
|
context = self._get_context(ParamKinds.bool, target, param, val)
|
||||||
|
context.run()
|
||||||
|
except ValueError as e:
|
||||||
|
self.logger.error(f"{e}... switching to float strategy")
|
||||||
|
context.strategy = FloatStrategy(target, param, val)
|
||||||
|
context.run()
|
||||||
|
case _:
|
||||||
|
self.logger.error(
|
||||||
|
f"unable to determine the kind of parameter from {cmd_parsed}"
|
||||||
|
)
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
def _get_context(self, kind, *args):
|
||||||
|
"""
|
||||||
|
determines a strategy for a kind of parameter and passes it to the context.
|
||||||
|
"""
|
||||||
|
|
||||||
|
match kind:
|
||||||
|
case ParamKinds.bool:
|
||||||
|
context = Context(BoolStrategy(*args))
|
||||||
|
case ParamKinds.float:
|
||||||
|
context = Context(FloatStrategy(*args))
|
||||||
|
case ParamKinds.string:
|
||||||
|
context = Context(StringStrategy(*args))
|
||||||
|
return context
|
||||||
|
|
||||||
|
|
||||||
def interactive_mode(parser):
|
def interactive_mode(parser):
|
||||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||||
if not cmd:
|
|
||||||
break
|
|
||||||
if res := parser.parse((cmd,)):
|
if res := parser.parse((cmd,)):
|
||||||
print(res)
|
print(res)
|
||||||
|
|
||||||
@@ -72,25 +173,23 @@ def interactive_mode(parser):
|
|||||||
def main():
|
def main():
|
||||||
# fmt: off
|
# fmt: off
|
||||||
cmds = (
|
cmds = (
|
||||||
"strip 0 -> mute -> on", "strip 0 -> mute", "bus 0 -> mute -> on",
|
"strip 0 -> mute -> true", "strip 0 -> mute", "bus 0 -> mute -> true",
|
||||||
"strip 0 -> mute -> off", "bus 0 -> mute -> on", "strip 3 -> solo -> on",
|
"strip 0 -> mute -> false", "bus 0 -> mute -> true", "strip 3 -> solo -> true",
|
||||||
"strip 3 -> solo -> off", "strip 1 -> A1 -> on", "strip 1 -> A1",
|
"strip 3 -> solo -> false", "strip 1 -> A1 -> true", "strip 1 -> A1",
|
||||||
"strip 1 -> A1 -> off", "strip 1 -> A1", "bus 3 -> eq -> on",
|
"strip 1 -> A1 -> false", "strip 1 -> A1", "strip 3 -> eq on -> true",
|
||||||
"bus 3 -> eq -> off", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
"bus 3 -> eq on -> false", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
||||||
"strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28",
|
"strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28",
|
||||||
"strip 2 -> limit",
|
"strip 2 -> limit", "strip 3 -> comp knob -> 3.8"
|
||||||
)
|
)
|
||||||
# fmt: on
|
# fmt: on
|
||||||
|
|
||||||
KIND_ID = "banana"
|
with voicemeeterlib.api("potato") as vm:
|
||||||
|
|
||||||
with voicemeeterlib.api(KIND_ID) as vm:
|
|
||||||
parser = Parser(vm)
|
parser = Parser(vm)
|
||||||
if args.i:
|
if args.i:
|
||||||
interactive_mode(parser)
|
interactive_mode(parser)
|
||||||
return
|
return
|
||||||
|
|
||||||
if res := parser.parse(cmds):
|
if res := parser.converter(cmds):
|
||||||
print(res)
|
print(res)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from logging import config
|
|
||||||
|
|
||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
|
|
||||||
@@ -18,6 +16,7 @@ class App:
|
|||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.vm.init_thread()
|
self.vm.init_thread()
|
||||||
|
return self
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, traceback):
|
def __exit__(self, exc_type, exc_value, traceback):
|
||||||
self.vm.end_thread()
|
self.vm.end_thread()
|
||||||
|
|||||||
@@ -14,19 +14,20 @@ class App:
|
|||||||
self.vm.observer.add(self.on_midi)
|
self.vm.observer.add(self.on_midi)
|
||||||
|
|
||||||
def on_midi(self):
|
def on_midi(self):
|
||||||
self.get_info()
|
if self.get_info() == self.MIDI_BUTTON:
|
||||||
self.on_midi_press()
|
self.on_midi_press()
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
current = self.vm.midi.current
|
current = self.vm.midi.current
|
||||||
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
|
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
|
||||||
|
return current
|
||||||
|
|
||||||
def on_midi_press(self):
|
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 (
|
if (
|
||||||
max(self.vm.strip[3].levels.postfader) > -40
|
self.vm.midi.get(self.MIDI_BUTTON) == 127
|
||||||
and self.vm.midi.get(self.MIDI_BUTTON) == 127
|
and max(self.vm.strip[3].levels.postfader) > -40
|
||||||
):
|
):
|
||||||
print(
|
print(
|
||||||
f"Strip 3 level max is greater than -40 and midi button {self.MIDI_BUTTON} is pressed"
|
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]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "22.12.0"
|
version = "24.3.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
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]
|
[package.dependencies]
|
||||||
click = ">=8.0.0"
|
click = ">=8.0.0"
|
||||||
mypy-extensions = ">=0.4.3"
|
mypy-extensions = ">=0.4.3"
|
||||||
|
packaging = ">=22.0"
|
||||||
pathspec = ">=0.9.0"
|
pathspec = ">=0.9.0"
|
||||||
platformdirs = ">=2"
|
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]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
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)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
@@ -23,25 +50,34 @@ uvloop = ["uvloop (>=0.15.2)"]
|
|||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.3.1"
|
version = "5.3.1"
|
||||||
description = "Extensible memoizing collections and decorators"
|
description = "Extensible memoizing collections and decorators"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "chardet"
|
name = "chardet"
|
||||||
version = "5.1.0"
|
version = "5.1.0"
|
||||||
description = "Universal encoding detector for Python 3"
|
description = "Universal encoding detector for Python 3"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.3"
|
version = "8.1.3"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
@@ -50,25 +86,34 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
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]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.3.6"
|
version = "0.3.6"
|
||||||
description = "Distribution utilities"
|
description = "Distribution utilities"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "exceptiongroup"
|
name = "exceptiongroup"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
description = "Backport of PEP 654 (exception groups)"
|
description = "Backport of PEP 654 (exception groups)"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.extras]
|
||||||
test = ["pytest (>=6)"]
|
test = ["pytest (>=6)"]
|
||||||
@@ -77,79 +122,103 @@ test = ["pytest (>=6)"]
|
|||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.12.2"
|
version = "3.12.2"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.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-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "2.0.0"
|
version = "2.0.0"
|
||||||
description = "brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "isort"
|
name = "isort"
|
||||||
version = "5.12.0"
|
version = "5.12.0"
|
||||||
description = "A Python utility / library to sort Python imports."
|
description = "A Python utility / library to sort Python imports."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8.0"
|
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]
|
[package.extras]
|
||||||
colors = ["colorama (>=0.4.3)"]
|
colors = ["colorama (>=0.4.3)"]
|
||||||
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
|
||||||
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
pipfile-deprecated-finder = ["pip-shims (>=0.5.2)", "pipreqs", "requirementslib"]
|
||||||
plugins = ["setuptools"]
|
plugins = ["setuptools"]
|
||||||
|
requirements-deprecated-finder = ["pip-api", "pipreqs"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy-extensions"
|
name = "mypy-extensions"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "Type system extensions for programs checked with the mypy type checker."
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.5"
|
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]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "23.1"
|
version = "23.1"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.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-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.3.1)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
@@ -159,25 +228,31 @@ testing = ["pytest", "pytest-benchmark"]
|
|||||||
name = "pyproject-api"
|
name = "pyproject-api"
|
||||||
version = "1.5.2"
|
version = "1.5.2"
|
||||||
description = "API to interact with the python pyproject.toml based projects"
|
description = "API to interact with the python pyproject.toml based projects"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
packaging = ">=23.1"
|
packaging = ">=23.1"
|
||||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
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-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
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]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.3.2"
|
version = "7.4.4"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
@@ -194,9 +269,12 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no
|
|||||||
name = "pytest-randomly"
|
name = "pytest-randomly"
|
||||||
version = "3.12.0"
|
version = "3.12.0"
|
||||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
@@ -205,28 +283,63 @@ pytest = "*"
|
|||||||
name = "pytest-repeat"
|
name = "pytest-repeat"
|
||||||
version = "0.9.1"
|
version = "0.9.1"
|
||||||
description = "pytest plugin for repeating tests"
|
description = "pytest plugin for repeating tests"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
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]
|
[package.dependencies]
|
||||||
pytest = ">=3.6"
|
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]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
category = "main"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]]
|
[[package]]
|
||||||
name = "tox"
|
name = "tox"
|
||||||
version = "4.6.3"
|
version = "4.6.3"
|
||||||
description = "tox is a generic virtualenv management and test command line tool"
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
cachetools = ">=5.3.1"
|
cachetools = ">=5.3.1"
|
||||||
@@ -241,16 +354,30 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
|||||||
virtualenv = ">=20.23.1"
|
virtualenv = ">=20.23.1"
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
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-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)"]
|
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]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "20.23.1"
|
version = "20.23.1"
|
||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
distlib = ">=0.3.6,<1"
|
distlib = ">=0.3.6,<1"
|
||||||
@@ -258,34 +385,10 @@ filelock = ">=3.12,<4"
|
|||||||
platformdirs = ">=3.5.1,<4"
|
platformdirs = ">=3.5.1,<4"
|
||||||
|
|
||||||
[package.extras]
|
[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)"]
|
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-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)"]
|
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]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
content-hash = "d4193db5ebc240288a21da72fa78788dfb71c08b6ecd25e365ce7fb1b0f27ac4"
|
||||||
|
|
||||||
[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 = []
|
|
||||||
|
|||||||
114
pyproject.toml
114
pyproject.toml
@@ -1,27 +1,26 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "voicemeeter-api"
|
name = "voicemeeter-api"
|
||||||
version = "2.0.2"
|
version = "2.6.0"
|
||||||
description = "A Python wrapper for the Voiceemeter API"
|
description = "A Python wrapper for the Voiceemeter API"
|
||||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
repository = "https://github.com/onyx-and-iris/voicemeeter-api-python"
|
repository = "https://github.com/onyx-and-iris/voicemeeter-api-python"
|
||||||
|
|
||||||
packages = [
|
packages = [{ include = "voicemeeterlib" }]
|
||||||
{ include = "voicemeeterlib" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
pytest = "^7.1.2"
|
pytest = "^7.4.4"
|
||||||
pytest-randomly = "^3.12.0"
|
pytest-randomly = "^3.12.0"
|
||||||
pytest-repeat = "^0.9.1"
|
pytest-repeat = "^0.9.1"
|
||||||
black = "^22.3.0"
|
black = ">=22.3,<25.0"
|
||||||
isort = "^5.10.1"
|
isort = "^5.10.1"
|
||||||
tox = "^4.6.3"
|
tox = "^4.6.3"
|
||||||
|
ruff = "^0.1.3"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
@@ -35,12 +34,15 @@ levels = "scripts:ex_levels"
|
|||||||
midi = "scripts:ex_midi"
|
midi = "scripts:ex_midi"
|
||||||
obs = "scripts:ex_obs"
|
obs = "scripts:ex_obs"
|
||||||
observer = "scripts:ex_observer"
|
observer = "scripts:ex_observer"
|
||||||
test = "scripts:test"
|
basic = "scripts:test_basic"
|
||||||
|
banana = "scripts:test_banana"
|
||||||
|
potato = "scripts:test_potato"
|
||||||
|
all = "scripts:test_all"
|
||||||
|
|
||||||
[tool.tox]
|
[tool.tox]
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py310,py311
|
envlist = py310,py311,py312
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
allowlist_externals = poetry
|
allowlist_externals = poetry
|
||||||
@@ -48,3 +50,97 @@ commands =
|
|||||||
poetry install -v
|
poetry install -v
|
||||||
poetry run pytest tests/
|
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 subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def ex_dsl():
|
def ex_dsl():
|
||||||
path = Path.cwd() / "examples" / "dsl" / "."
|
scriptpath = Path.cwd() / "examples" / "dsl" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_events():
|
def ex_events():
|
||||||
path = Path.cwd() / "examples" / "events" / "."
|
scriptpath = Path.cwd() / "examples" / "events" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_gui():
|
def ex_gui():
|
||||||
path = Path.cwd() / "examples" / "gui" / "."
|
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_levels():
|
def ex_levels():
|
||||||
path = Path.cwd() / "examples" / "levels" / "."
|
scriptpath = Path.cwd() / "examples" / "levels" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_midi():
|
def ex_midi():
|
||||||
path = Path.cwd() / "examples" / "midi" / "."
|
scriptpath = Path.cwd() / "examples" / "midi" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_obs():
|
def ex_obs():
|
||||||
path = Path.cwd() / "examples" / "obs" / "."
|
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_observer():
|
def ex_observer():
|
||||||
path = Path.cwd() / "examples" / "observer" / "."
|
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def test():
|
def test_basic():
|
||||||
|
os.environ["KIND"] = "basic"
|
||||||
subprocess.run(["tox"])
|
subprocess.run(["tox"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_banana():
|
||||||
|
os.environ["KIND"] = "banana"
|
||||||
|
subprocess.run(["tox"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_potato():
|
||||||
|
os.environ["KIND"] = "potato"
|
||||||
|
subprocess.run(["tox"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_all():
|
||||||
|
steps = [test_basic, test_banana, test_potato]
|
||||||
|
[step() for step in steps]
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -30,8 +31,10 @@ class Data:
|
|||||||
return (2 * self.phys_in) + (8 * self.virt_in)
|
return (2 * self.phys_in) + (8 * self.virt_in)
|
||||||
|
|
||||||
|
|
||||||
# let's keep things random
|
# get KIND_ID from env var, otherwise set to random
|
||||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
KIND_ID = os.environ.get(
|
||||||
|
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||||
|
)
|
||||||
vm = voicemeeterlib.api(KIND_ID)
|
vm = voicemeeterlib.api(KIND_ID)
|
||||||
kind = kindmap(KIND_ID)
|
kind = kindmap(KIND_ID)
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 155"><title>tests: 155</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">155</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">155</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 159"><title>tests: 159</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">159</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">159</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 112"><title>tests: 112</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">112</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">112</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 116"><title>tests: 116</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">116</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">116</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 164"><title>tests: 164</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">164</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">164</text></g></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 184"><title>tests: 184</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">184</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">184</text></g></svg>
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -25,7 +25,11 @@ Function Get-TimeStamp {
|
|||||||
if ($MyInvocation.InvocationName -ne ".") {
|
if ($MyInvocation.InvocationName -ne ".") {
|
||||||
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
||||||
|
|
||||||
|
@("potato") | ForEach-Object {
|
||||||
|
$env:KIND = $_
|
||||||
RunTests
|
RunTests
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Invoke-Expression "deactivate"
|
Invoke-Expression "deactivate"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,37 +12,37 @@ class TestUserConfigs:
|
|||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
vm.apply_config("example")
|
vm.apply_config("example")
|
||||||
|
|
||||||
def test_it_vm_config_string(self):
|
def test_it_tests_vm_config_string(self):
|
||||||
assert "PhysStrip" in vm.strip[data.phys_in].label
|
assert "PhysStrip" in vm.strip[data.phys_in].label
|
||||||
assert "VirtStrip" in vm.strip[data.virt_in].label
|
assert "VirtStrip" in vm.strip[data.virt_in].label
|
||||||
assert "PhysBus" in vm.bus[data.phys_out].label
|
assert "PhysBus" in vm.bus[data.phys_out].label
|
||||||
assert "VirtBus" in vm.bus[data.virt_out].label
|
assert "VirtBus" in vm.bus[data.virt_out].label
|
||||||
|
|
||||||
def test_it_vm_config_bool(self):
|
def test_it_tests_vm_config_bool(self):
|
||||||
assert vm.strip[0].A1 == True
|
assert vm.strip[0].A1 == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
reason="Skip test if kind is not potato",
|
reason="Skip test if kind is not potato",
|
||||||
)
|
)
|
||||||
def test_it_vm_config_bool_strip_eq_on(self):
|
def test_it_tests_vm_config_bool_strip_eq_on(self):
|
||||||
assert vm.strip[data.phys_in].eq.on == True
|
assert vm.strip[data.phys_in].eq.on == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
reason="Skip test if kind is not banana",
|
reason="Skip test if kind is not banana",
|
||||||
)
|
)
|
||||||
def test_it_vm_config_bool_bus_eq_ab(self):
|
def test_it_tests_vm_config_bool_bus_eq_ab(self):
|
||||||
assert vm.bus[data.phys_out].eq.ab == True
|
assert vm.bus[data.phys_out].eq.ab == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"not config.getoption('--run-slow')",
|
"not config.getoption('--run-slow')",
|
||||||
reason="Only run when --run-slow is given",
|
reason="Only run when --run-slow is given",
|
||||||
)
|
)
|
||||||
def test_it_vm_config_busmode(self):
|
def test_it_tests_vm_config_busmode(self):
|
||||||
assert vm.bus[data.phys_out].mode.get() == "composite"
|
assert vm.bus[data.phys_out].mode.get() == "composite"
|
||||||
|
|
||||||
def test_it_vm_config_bass_med_high(self):
|
def test_it_tests_vm_config_bass_med_high(self):
|
||||||
assert vm.strip[data.virt_in].bass == -3.2
|
assert vm.strip[data.virt_in].bass == -3.2
|
||||||
assert vm.strip[data.virt_in].mid == 1.5
|
assert vm.strip[data.virt_in].mid == 1.5
|
||||||
assert vm.strip[data.virt_in].high == 2.1
|
assert vm.strip[data.virt_in].high == 2.1
|
||||||
|
|||||||
49
tests/test_errors.py
Normal file
49
tests/test_errors.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
import voicemeeterlib
|
||||||
|
from tests import data, vm
|
||||||
|
|
||||||
|
|
||||||
|
class TestErrors:
|
||||||
|
__test__ = True
|
||||||
|
|
||||||
|
def test_it_tests_an_unknown_kind(self):
|
||||||
|
with pytest.raises(
|
||||||
|
voicemeeterlib.error.VMError,
|
||||||
|
match="Unknown Voicemeeter kind 'unknown_kind'",
|
||||||
|
):
|
||||||
|
voicemeeterlib.api("unknown_kind")
|
||||||
|
|
||||||
|
def test_it_tests_an_unknown_parameter(self):
|
||||||
|
with pytest.raises(
|
||||||
|
voicemeeterlib.error.CAPIError,
|
||||||
|
match="VBVMR_SetParameterFloat returned -3",
|
||||||
|
) as exc_info:
|
||||||
|
vm.set("unknown.parameter", 1)
|
||||||
|
|
||||||
|
e = exc_info.value
|
||||||
|
assert e.code == -3
|
||||||
|
assert e.fn_name == "VBVMR_SetParameterFloat"
|
||||||
|
|
||||||
|
def test_it_tests_an_unknown_config_name(self):
|
||||||
|
EXPECTED_MSG = (
|
||||||
|
"No config with name 'unknown' is loaded into memory",
|
||||||
|
f"Known configs: {list(vm.configs.keys())}",
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
voicemeeterlib.error.VMError, match=re.escape("\n".join(EXPECTED_MSG))
|
||||||
|
):
|
||||||
|
vm.apply_config("unknown")
|
||||||
|
|
||||||
|
def test_it_tests_an_invalid_config_key(self):
|
||||||
|
CONFIG = {
|
||||||
|
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
||||||
|
"bus-0": {"mute": True, "eq": {"on": True}},
|
||||||
|
"unknown-0": {"state": True},
|
||||||
|
"vban-out-1": {"name": "streamname"},
|
||||||
|
}
|
||||||
|
with pytest.raises(ValueError, match="invalid config key 'unknown-0'"):
|
||||||
|
vm.apply(CONFIG)
|
||||||
@@ -10,7 +10,7 @@ class TestRemoteFactories:
|
|||||||
data.name != "basic",
|
data.name != "basic",
|
||||||
reason="Skip test if kind is not basic",
|
reason="Skip test if kind is not basic",
|
||||||
)
|
)
|
||||||
def test_it_vm_remote_attrs_for_basic(self):
|
def test_it_tests_vm_remote_attrs_for_basic(self):
|
||||||
assert hasattr(vm, "strip")
|
assert hasattr(vm, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
@@ -22,13 +22,13 @@ class TestRemoteFactories:
|
|||||||
assert len(vm.strip) == 3
|
assert len(vm.strip) == 3
|
||||||
assert len(vm.bus) == 2
|
assert len(vm.bus) == 2
|
||||||
assert len(vm.button) == 80
|
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(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
reason="Skip test if kind is not banana",
|
reason="Skip test if kind is not banana",
|
||||||
)
|
)
|
||||||
def test_it_vm_remote_attrs_for_banana(self):
|
def test_it_tests_vm_remote_attrs_for_banana(self):
|
||||||
assert hasattr(vm, "strip")
|
assert hasattr(vm, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
@@ -42,13 +42,13 @@ class TestRemoteFactories:
|
|||||||
assert len(vm.strip) == 5
|
assert len(vm.strip) == 5
|
||||||
assert len(vm.bus) == 5
|
assert len(vm.bus) == 5
|
||||||
assert len(vm.button) == 80
|
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(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
reason="Skip test if kind is not potato",
|
reason="Skip test if kind is not potato",
|
||||||
)
|
)
|
||||||
def test_it_vm_remote_attrs_for_potato(self):
|
def test_it_tests_vm_remote_attrs_for_potato(self):
|
||||||
assert hasattr(vm, "strip")
|
assert hasattr(vm, "strip")
|
||||||
assert hasattr(vm, "bus")
|
assert hasattr(vm, "bus")
|
||||||
assert hasattr(vm, "command")
|
assert hasattr(vm, "command")
|
||||||
@@ -63,4 +63,4 @@ class TestRemoteFactories:
|
|||||||
assert len(vm.strip) == 8
|
assert len(vm.strip) == 8
|
||||||
assert len(vm.bus) == 8
|
assert len(vm.bus) == 8
|
||||||
assert len(vm.button) == 80
|
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):
|
class VirtualBus(Bus):
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, kind):
|
def make(cls, remote, i, kind):
|
||||||
"""
|
"""
|
||||||
Factory method for VirtualBus.
|
Factory method for VirtualBus.
|
||||||
|
|
||||||
@@ -183,7 +183,13 @@ class VirtualBus(Bus):
|
|||||||
"""
|
"""
|
||||||
kls = (cls,)
|
kls = (cls,)
|
||||||
if kind.name == "basic":
|
if kind.name == "basic":
|
||||||
kls += (PhysicalBus,)
|
return type(
|
||||||
|
"VirtualBus",
|
||||||
|
kls,
|
||||||
|
{
|
||||||
|
"device": BusDevice.make(remote, i),
|
||||||
|
},
|
||||||
|
)
|
||||||
elif kind.name == "potato":
|
elif kind.name == "potato":
|
||||||
EFFECTS_cls = _make_effects_mixin()
|
EFFECTS_cls = _make_effects_mixin()
|
||||||
kls += (EFFECTS_cls,)
|
kls += (EFFECTS_cls,)
|
||||||
@@ -210,7 +216,7 @@ class BusLevel(IRemote):
|
|||||||
def fget(x):
|
def fget(x):
|
||||||
return round(20 * log(x, 10), 1) if x > 0 else -200.0
|
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]]
|
vals = self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
||||||
else:
|
else:
|
||||||
vals = [self._remote.get_level(mode, i) for i in range(*self.range)]
|
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.
|
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]])
|
return any(self._remote._bus_comp[self.range[0] : self.range[-1]])
|
||||||
|
|
||||||
is_updated = isdirty
|
is_updated = isdirty
|
||||||
@@ -306,7 +312,7 @@ def bus_factory(is_phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
|||||||
BUS_cls = (
|
BUS_cls = (
|
||||||
PhysicalBus.make(remote, i, remote.kind)
|
PhysicalBus.make(remote, i, remote.kind)
|
||||||
if is_phys_bus
|
if is_phys_bus
|
||||||
else VirtualBus.make(remote.kind)
|
else VirtualBus.make(remote, i, remote.kind)
|
||||||
)
|
)
|
||||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||||
return type(
|
return type(
|
||||||
|
|||||||
@@ -16,109 +16,109 @@ class CBindings(metaclass=ABCMeta):
|
|||||||
Maps expected ctype argument and res types for each binding.
|
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
|
bind_login = libc.VBVMR_Login
|
||||||
vm_login.restype = LONG
|
bind_login.restype = LONG
|
||||||
vm_login.argtypes = None
|
bind_login.argtypes = None
|
||||||
|
|
||||||
vm_logout = libc.VBVMR_Logout
|
bind_logout = libc.VBVMR_Logout
|
||||||
vm_logout.restype = LONG
|
bind_logout.restype = LONG
|
||||||
vm_logout.argtypes = None
|
bind_logout.argtypes = None
|
||||||
|
|
||||||
vm_runvm = libc.VBVMR_RunVoicemeeter
|
bind_run_voicemeeter = libc.VBVMR_RunVoicemeeter
|
||||||
vm_runvm.restype = LONG
|
bind_run_voicemeeter.restype = LONG
|
||||||
vm_runvm.argtypes = [LONG]
|
bind_run_voicemeeter.argtypes = [LONG]
|
||||||
|
|
||||||
vm_get_type = libc.VBVMR_GetVoicemeeterType
|
bind_get_voicemeeter_type = libc.VBVMR_GetVoicemeeterType
|
||||||
vm_get_type.restype = LONG
|
bind_get_voicemeeter_type.restype = LONG
|
||||||
vm_get_type.argtypes = [ct.POINTER(LONG)]
|
bind_get_voicemeeter_type.argtypes = [ct.POINTER(LONG)]
|
||||||
|
|
||||||
vm_get_version = libc.VBVMR_GetVoicemeeterVersion
|
bind_get_voicemeeter_version = libc.VBVMR_GetVoicemeeterVersion
|
||||||
vm_get_version.restype = LONG
|
bind_get_voicemeeter_version.restype = LONG
|
||||||
vm_get_version.argtypes = [ct.POINTER(LONG)]
|
bind_get_voicemeeter_version.argtypes = [ct.POINTER(LONG)]
|
||||||
|
|
||||||
if hasattr(libc, "VBVMR_MacroButton_IsDirty"):
|
if hasattr(libc, "VBVMR_MacroButton_IsDirty"):
|
||||||
vm_mdirty = libc.VBVMR_MacroButton_IsDirty
|
bind_macro_button_is_dirty = libc.VBVMR_MacroButton_IsDirty
|
||||||
vm_mdirty.restype = LONG
|
bind_macro_button_is_dirty.restype = LONG
|
||||||
vm_mdirty.argtypes = None
|
bind_macro_button_is_dirty.argtypes = None
|
||||||
|
|
||||||
if hasattr(libc, "VBVMR_MacroButton_GetStatus"):
|
if hasattr(libc, "VBVMR_MacroButton_GetStatus"):
|
||||||
vm_get_buttonstatus = libc.VBVMR_MacroButton_GetStatus
|
bind_macro_button_get_status = libc.VBVMR_MacroButton_GetStatus
|
||||||
vm_get_buttonstatus.restype = LONG
|
bind_macro_button_get_status.restype = LONG
|
||||||
vm_get_buttonstatus.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
bind_macro_button_get_status.argtypes = [LONG, ct.POINTER(FLOAT), LONG]
|
||||||
|
|
||||||
if hasattr(libc, "VBVMR_MacroButton_SetStatus"):
|
if hasattr(libc, "VBVMR_MacroButton_SetStatus"):
|
||||||
vm_set_buttonstatus = libc.VBVMR_MacroButton_SetStatus
|
bind_macro_button_set_status = libc.VBVMR_MacroButton_SetStatus
|
||||||
vm_set_buttonstatus.restype = LONG
|
bind_macro_button_set_status.restype = LONG
|
||||||
vm_set_buttonstatus.argtypes = [LONG, FLOAT, LONG]
|
bind_macro_button_set_status.argtypes = [LONG, FLOAT, LONG]
|
||||||
|
|
||||||
vm_pdirty = libc.VBVMR_IsParametersDirty
|
bind_is_parameters_dirty = libc.VBVMR_IsParametersDirty
|
||||||
vm_pdirty.restype = LONG
|
bind_is_parameters_dirty.restype = LONG
|
||||||
vm_pdirty.argtypes = None
|
bind_is_parameters_dirty.argtypes = None
|
||||||
|
|
||||||
vm_get_parameter_float = libc.VBVMR_GetParameterFloat
|
bind_get_parameter_float = libc.VBVMR_GetParameterFloat
|
||||||
vm_get_parameter_float.restype = LONG
|
bind_get_parameter_float.restype = LONG
|
||||||
vm_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
bind_get_parameter_float.argtypes = [ct.POINTER(CHAR), ct.POINTER(FLOAT)]
|
||||||
|
|
||||||
vm_set_parameter_float = libc.VBVMR_SetParameterFloat
|
bind_set_parameter_float = libc.VBVMR_SetParameterFloat
|
||||||
vm_set_parameter_float.restype = LONG
|
bind_set_parameter_float.restype = LONG
|
||||||
vm_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
bind_set_parameter_float.argtypes = [ct.POINTER(CHAR), FLOAT]
|
||||||
|
|
||||||
vm_get_parameter_string = libc.VBVMR_GetParameterStringW
|
bind_get_parameter_string_w = libc.VBVMR_GetParameterStringW
|
||||||
vm_get_parameter_string.restype = LONG
|
bind_get_parameter_string_w.restype = LONG
|
||||||
vm_get_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
bind_get_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR * 512)]
|
||||||
|
|
||||||
vm_set_parameter_string = libc.VBVMR_SetParameterStringW
|
bind_set_parameter_string_w = libc.VBVMR_SetParameterStringW
|
||||||
vm_set_parameter_string.restype = LONG
|
bind_set_parameter_string_w.restype = LONG
|
||||||
vm_set_parameter_string.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
bind_set_parameter_string_w.argtypes = [ct.POINTER(CHAR), ct.POINTER(WCHAR)]
|
||||||
|
|
||||||
vm_set_parameter_multi = libc.VBVMR_SetParameters
|
bind_set_parameters = libc.VBVMR_SetParameters
|
||||||
vm_set_parameter_multi.restype = LONG
|
bind_set_parameters.restype = LONG
|
||||||
vm_set_parameter_multi.argtypes = [ct.POINTER(CHAR)]
|
bind_set_parameters.argtypes = [ct.POINTER(CHAR)]
|
||||||
|
|
||||||
vm_get_level = libc.VBVMR_GetLevel
|
bind_get_level = libc.VBVMR_GetLevel
|
||||||
vm_get_level.restype = LONG
|
bind_get_level.restype = LONG
|
||||||
vm_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
bind_get_level.argtypes = [LONG, LONG, ct.POINTER(FLOAT)]
|
||||||
|
|
||||||
vm_get_num_indevices = libc.VBVMR_Input_GetDeviceNumber
|
bind_input_get_device_number = libc.VBVMR_Input_GetDeviceNumber
|
||||||
vm_get_num_indevices.restype = LONG
|
bind_input_get_device_number.restype = LONG
|
||||||
vm_get_num_indevices.argtypes = None
|
bind_input_get_device_number.argtypes = None
|
||||||
|
|
||||||
vm_get_desc_indevices = libc.VBVMR_Input_GetDeviceDescW
|
bind_input_get_device_desc_w = libc.VBVMR_Input_GetDeviceDescW
|
||||||
vm_get_desc_indevices.restype = LONG
|
bind_input_get_device_desc_w.restype = LONG
|
||||||
vm_get_desc_indevices.argtypes = [
|
bind_input_get_device_desc_w.argtypes = [
|
||||||
LONG,
|
LONG,
|
||||||
ct.POINTER(LONG),
|
ct.POINTER(LONG),
|
||||||
ct.POINTER(WCHAR * 256),
|
ct.POINTER(WCHAR * 256),
|
||||||
ct.POINTER(WCHAR * 256),
|
ct.POINTER(WCHAR * 256),
|
||||||
]
|
]
|
||||||
|
|
||||||
vm_get_num_outdevices = libc.VBVMR_Output_GetDeviceNumber
|
bind_output_get_device_number = libc.VBVMR_Output_GetDeviceNumber
|
||||||
vm_get_num_outdevices.restype = LONG
|
bind_output_get_device_number.restype = LONG
|
||||||
vm_get_num_outdevices.argtypes = None
|
bind_output_get_device_number.argtypes = None
|
||||||
|
|
||||||
vm_get_desc_outdevices = libc.VBVMR_Output_GetDeviceDescW
|
bind_output_get_device_desc_w = libc.VBVMR_Output_GetDeviceDescW
|
||||||
vm_get_desc_outdevices.restype = LONG
|
bind_output_get_device_desc_w.restype = LONG
|
||||||
vm_get_desc_outdevices.argtypes = [
|
bind_output_get_device_desc_w.argtypes = [
|
||||||
LONG,
|
LONG,
|
||||||
ct.POINTER(LONG),
|
ct.POINTER(LONG),
|
||||||
ct.POINTER(WCHAR * 256),
|
ct.POINTER(WCHAR * 256),
|
||||||
ct.POINTER(WCHAR * 256),
|
ct.POINTER(WCHAR * 256),
|
||||||
]
|
]
|
||||||
|
|
||||||
vm_get_midi_message = libc.VBVMR_GetMidiMessage
|
bind_get_midi_message = libc.VBVMR_GetMidiMessage
|
||||||
vm_get_midi_message.restype = LONG
|
bind_get_midi_message.restype = LONG
|
||||||
vm_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
bind_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
|
||||||
|
|
||||||
def call(self, func, *args, ok=(0,), ok_exp=None):
|
def call(self, func, *args, ok=(0,), ok_exp=None):
|
||||||
try:
|
try:
|
||||||
res = func(*args)
|
res = func(*args)
|
||||||
if ok_exp is None:
|
if ok_exp is None:
|
||||||
if res not in ok:
|
if res not in ok:
|
||||||
raise CAPIError(f"{func.__name__} returned {res}")
|
raise CAPIError(func.__name__, res)
|
||||||
elif not ok_exp(res):
|
elif not ok_exp(res) and res not in ok:
|
||||||
raise CAPIError(f"{func.__name__} returned {res}")
|
raise CAPIError(func.__name__, res)
|
||||||
return res
|
return res
|
||||||
except CAPIError as e:
|
except CAPIError as e:
|
||||||
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
self.logger_cbindings.exception(f"{type(e).__name__}: {e}")
|
||||||
|
|||||||
@@ -147,8 +147,13 @@ class Loader(metaclass=SingletonType):
|
|||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"config file with name {identifier} already in memory, skipping.."
|
f"config file with name {identifier} already in memory, skipping.."
|
||||||
)
|
)
|
||||||
return False
|
return
|
||||||
|
try:
|
||||||
self.parser = dataextraction_factory(data)
|
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
|
return True
|
||||||
|
|
||||||
def register(self, identifier, data=None):
|
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"""
|
"""Exception raised when installation errors occur"""
|
||||||
|
|
||||||
|
|
||||||
class CAPIError(Exception):
|
class CAPIError(VMError):
|
||||||
"""Exception raised when the C-API returns error values"""
|
"""Exception raised when the C-API returns an error code"""
|
||||||
|
|
||||||
|
def __init__(self, fn_name, code):
|
||||||
class VMError(Exception):
|
self.fn_name = fn_name
|
||||||
"""Exception raised when general errors occur"""
|
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():
|
if self.any():
|
||||||
info += (f"now listening for {', '.join(self.get())} events",)
|
info += (f"now listening for {', '.join(self.get())} events",)
|
||||||
else:
|
else:
|
||||||
info += (f"not listening for any events",)
|
info += ("not listening for any events",)
|
||||||
self.logger.info(", ".join(info))
|
self.logger.info(", ".join(info))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import logging
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Iterable, NoReturn
|
from typing import Iterable
|
||||||
|
|
||||||
from . import misc
|
from . import misc
|
||||||
from .bus import request_bus_obj as bus
|
from .bus import request_bus_obj as bus
|
||||||
@@ -51,7 +51,7 @@ class FactoryBuilder:
|
|||||||
)
|
)
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
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"""
|
"""prints progress status for each step"""
|
||||||
name = name.split("_")[1]
|
name = name.split("_")[1]
|
||||||
self.logger.debug(self._info[int(getattr(self.BuilderProgress, name))])
|
self.logger.debug(self._info[int(getattr(self.BuilderProgress, name))])
|
||||||
@@ -114,6 +114,8 @@ class FactoryBase(Remote):
|
|||||||
"mdirty": False,
|
"mdirty": False,
|
||||||
"midi": False,
|
"midi": False,
|
||||||
"ldirty": False,
|
"ldirty": False,
|
||||||
|
"timeout": 2,
|
||||||
|
"bits": 64,
|
||||||
}
|
}
|
||||||
if "subs" in kwargs:
|
if "subs" in kwargs:
|
||||||
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
||||||
|
|||||||
@@ -5,36 +5,41 @@ from pathlib import Path
|
|||||||
|
|
||||||
from .error import InstallError
|
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":
|
if platform.system() != "Windows":
|
||||||
raise InstallError("Only Windows OS supported")
|
raise InstallError("Only Windows OS supported")
|
||||||
|
|
||||||
|
|
||||||
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
VM_KEY = "VB:Voicemeeter {17359A74-1236-5467}"
|
||||||
REG_KEY = "".join(
|
REG_KEY = "\\".join(
|
||||||
[
|
filter(
|
||||||
|
None,
|
||||||
|
(
|
||||||
"SOFTWARE",
|
"SOFTWARE",
|
||||||
("\\WOW6432Node" if bits == 64 else ""),
|
"WOW6432Node" if BITS == 64 else "",
|
||||||
"\\Microsoft\\Windows\\CurrentVersion\\Uninstall",
|
"Microsoft",
|
||||||
]
|
"Windows",
|
||||||
|
"CurrentVersion",
|
||||||
|
"Uninstall",
|
||||||
|
),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_vmpath():
|
def get_vmpath():
|
||||||
with winreg.OpenKey(
|
with winreg.OpenKey(
|
||||||
winreg.HKEY_LOCAL_MACHINE, r"{}".format(REG_KEY + "\\" + VM_KEY)
|
winreg.HKEY_LOCAL_MACHINE, r"{}".format("\\".join((REG_KEY, VM_KEY)))
|
||||||
) as vm_key:
|
) as vm_key:
|
||||||
return winreg.QueryValueEx(vm_key, r"UninstallString")[0]
|
return winreg.QueryValueEx(vm_key, r"UninstallString")[0].strip('"')
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
vm_path = Path(get_vmpath())
|
vm_parent = Path(get_vmpath()).parent
|
||||||
except FileNotFoundError as e:
|
except FileNotFoundError as e:
|
||||||
raise InstallError(f"Unable to fetch DLL path from the registry") from e
|
raise InstallError("Unable to fetch DLL path from the registry") from e
|
||||||
vm_parent = vm_path.parent
|
|
||||||
|
|
||||||
DLL_NAME = f'VoicemeeterRemote{"64" if bits == 64 else ""}.dll'
|
DLL_NAME = f'VoicemeeterRemote{"64" if BITS == 64 else ""}.dll'
|
||||||
|
|
||||||
dll_path = vm_parent.joinpath(DLL_NAME)
|
dll_path = vm_parent.joinpath(DLL_NAME)
|
||||||
if not dll_path.is_file():
|
if not dll_path.is_file():
|
||||||
|
|||||||
@@ -30,31 +30,40 @@ class KindMapClass(metaclass=SingletonType):
|
|||||||
vban: tuple
|
vban: tuple
|
||||||
asio: tuple
|
asio: tuple
|
||||||
insert: int
|
insert: int
|
||||||
|
composite: int
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def phys_in(self):
|
def phys_in(self) -> int:
|
||||||
return self.ins[0]
|
return self.ins[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def virt_in(self):
|
def virt_in(self) -> int:
|
||||||
return self.ins[-1]
|
return self.ins[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def phys_out(self):
|
def phys_out(self) -> int:
|
||||||
return self.outs[0]
|
return self.outs[0]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def virt_out(self):
|
def virt_out(self) -> int:
|
||||||
return self.outs[-1]
|
return self.outs[-1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_strip(self):
|
def num_strip(self) -> int:
|
||||||
return sum(self.ins)
|
return sum(self.ins)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def num_bus(self):
|
def num_bus(self) -> int:
|
||||||
return sum(self.outs)
|
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:
|
def __str__(self) -> str:
|
||||||
return self.name.capitalize()
|
return self.name.capitalize()
|
||||||
|
|
||||||
@@ -64,9 +73,10 @@ class BasicMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (2, 1)
|
ins: tuple = (2, 1)
|
||||||
outs: tuple = (1, 1)
|
outs: tuple = (1, 1)
|
||||||
vban: tuple = (4, 4)
|
vban: tuple = (4, 4, 1, 1)
|
||||||
asio: tuple = (0, 0)
|
asio: tuple = (0, 0)
|
||||||
insert: int = 0
|
insert: int = 0
|
||||||
|
composite: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -74,9 +84,10 @@ class BananaMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (3, 2)
|
ins: tuple = (3, 2)
|
||||||
outs: tuple = (3, 2)
|
outs: tuple = (3, 2)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
asio: tuple = (6, 8)
|
asio: tuple = (6, 8)
|
||||||
insert: int = 22
|
insert: int = 22
|
||||||
|
composite: int = 8
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -84,9 +95,10 @@ class PotatoMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (5, 3)
|
ins: tuple = (5, 3)
|
||||||
outs: tuple = (5, 3)
|
outs: tuple = (5, 3)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
asio: tuple = (10, 8)
|
asio: tuple = (10, 8)
|
||||||
insert: int = 34
|
insert: int = 34
|
||||||
|
composite: int = 8
|
||||||
|
|
||||||
|
|
||||||
def kind_factory(kind_id):
|
def kind_factory(kind_id):
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
|
from enum import IntEnum
|
||||||
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
|
|
||||||
|
ButtonModes = IntEnum(
|
||||||
|
"ButtonModes",
|
||||||
|
"state stateonly trigger",
|
||||||
|
start=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Adapter(IRemote):
|
class Adapter(IRemote):
|
||||||
"""Adapter to the common interface."""
|
"""Adapter to the common interface."""
|
||||||
@@ -8,9 +16,13 @@ class Adapter(IRemote):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
def getter(self, mode):
|
def getter(self, mode):
|
||||||
|
self.logger.debug(f"getter: button[{self.index}].{ButtonModes(mode).name}")
|
||||||
return self._remote.get_buttonstatus(self.index, mode)
|
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)
|
self._remote.set_buttonstatus(self.index, val, mode)
|
||||||
|
|
||||||
|
|
||||||
@@ -22,24 +34,24 @@ class MacroButton(Adapter):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> bool:
|
def state(self) -> bool:
|
||||||
return self.getter(1) == 1
|
return self.getter(ButtonModes.state) == 1
|
||||||
|
|
||||||
@state.setter
|
@state.setter
|
||||||
def state(self, val):
|
def state(self, val: bool):
|
||||||
self.setter(1 if val else 0, 1)
|
self.setter(ButtonModes.state, 1 if val else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stateonly(self) -> bool:
|
def stateonly(self) -> bool:
|
||||||
return self.getter(2) == 1
|
return self.getter(ButtonModes.stateonly) == 1
|
||||||
|
|
||||||
@stateonly.setter
|
@stateonly.setter
|
||||||
def stateonly(self, val):
|
def stateonly(self, val: bool):
|
||||||
self.setter(1 if val else 0, 2)
|
self.setter(ButtonModes.stateonly, 1 if val else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trigger(self) -> bool:
|
def trigger(self) -> bool:
|
||||||
return self.getter(3) == 1
|
return self.getter(ButtonModes.trigger) == 1
|
||||||
|
|
||||||
@trigger.setter
|
@trigger.setter
|
||||||
def trigger(self, val):
|
def trigger(self, val: bool):
|
||||||
self.setter(1 if val else 0, 3)
|
self.setter(ButtonModes.trigger, 1 if val else 0)
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .error import VMError
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .kinds import kinds_all
|
from .kinds import kinds_all
|
||||||
|
|
||||||
@@ -71,7 +70,7 @@ class Patch(IRemote):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"patch"
|
return "patch"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def postfadercomp(self) -> bool:
|
def postfadercomp(self) -> bool:
|
||||||
@@ -93,7 +92,7 @@ class Patch(IRemote):
|
|||||||
class Asio(IRemote):
|
class Asio(IRemote):
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"patch"
|
return "patch"
|
||||||
|
|
||||||
|
|
||||||
class AsioIn(Asio):
|
class AsioIn(Asio):
|
||||||
@@ -196,7 +195,7 @@ class Option(IRemote):
|
|||||||
def sr(self, val: int):
|
def sr(self, val: int):
|
||||||
opts = (44100, 48000, 88200, 96000, 176400, 192000)
|
opts = (44100, 48000, 88200, 96000, 176400, 192000)
|
||||||
if val not in opts:
|
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)
|
self.setter("sr", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class Recorder(IRemote):
|
|||||||
|
|
||||||
time_str = str(time_str) # coerce the type
|
time_str = str(time_str) # coerce the type
|
||||||
if (
|
if (
|
||||||
match := re.match(
|
re.match(
|
||||||
r"^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$",
|
r"^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$",
|
||||||
time_str,
|
time_str,
|
||||||
)
|
)
|
||||||
@@ -135,7 +135,7 @@ class Recorder(IRemote):
|
|||||||
self.setter("goto", get_sec())
|
self.setter("goto", get_sec())
|
||||||
else:
|
else:
|
||||||
self.logger.warning(
|
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):
|
def filetype(self, val: str):
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
import ctypes as ct
|
import ctypes as ct
|
||||||
import logging
|
import logging
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Iterable, NoReturn, Optional, Union
|
from typing import Iterable, Optional, Union
|
||||||
|
|
||||||
from .cbindings import CBindings
|
from .cbindings import CBindings
|
||||||
from .error import CAPIError, VMError
|
from .error import CAPIError, VMError
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .inst import bits
|
from .inst import BITS
|
||||||
from .kinds import KindId
|
from .kinds import KindId
|
||||||
from .misc import Midi, VmGui
|
from .misc import Midi, VmGui
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
from .updater import Producer, Updater
|
from .updater import Producer, Updater
|
||||||
from .util import grouper, polling, script
|
from .util import deep_merge, grouper, polling, script, timeout
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -28,16 +29,22 @@ class Remote(CBindings):
|
|||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.midi = Midi()
|
self.midi = Midi()
|
||||||
self.subject = self.observer = Subject()
|
self.subject = self.observer = Subject()
|
||||||
self.running = False
|
|
||||||
self.event = Event(
|
self.event = Event(
|
||||||
{k: kwargs.pop(k) for k in ("pdirty", "mdirty", "midi", "ldirty")}
|
{k: kwargs.pop(k) for k in ("pdirty", "mdirty", "midi", "ldirty")}
|
||||||
)
|
)
|
||||||
self.gui = VmGui()
|
self.gui = VmGui()
|
||||||
|
self.stop_event = None
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
for attr, val in kwargs.items():
|
for attr, val in kwargs.items():
|
||||||
setattr(self, attr, val)
|
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):
|
def __enter__(self):
|
||||||
"""setup procedures"""
|
"""setup procedures"""
|
||||||
self.login()
|
self.login()
|
||||||
@@ -52,49 +59,50 @@ class Remote(CBindings):
|
|||||||
|
|
||||||
def init_thread(self):
|
def init_thread(self):
|
||||||
"""Starts updates thread."""
|
"""Starts updates thread."""
|
||||||
self.running = True
|
|
||||||
self.event.info()
|
self.event.info()
|
||||||
|
|
||||||
self.logger.debug("initiating events thread")
|
self.logger.debug("initiating events thread")
|
||||||
|
self.stop_event = threading.Event()
|
||||||
|
self.stop_event.clear()
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
self.updater = Updater(self, queue)
|
self.updater = Updater(self, queue)
|
||||||
self.updater.start()
|
self.updater.start()
|
||||||
self.producer = Producer(self, queue)
|
self.producer = Producer(self, queue, self.stop_event)
|
||||||
self.producer.start()
|
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"""
|
"""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:
|
if not self.gui.launched:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
|
"Voicemeeter engine running but GUI not launched. Launching the GUI now."
|
||||||
)
|
)
|
||||||
self.run_voicemeeter(self.kind.name)
|
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):
|
if kind_id not in (kind.name.lower() for kind in KindId):
|
||||||
raise VMError(f"Unexpected Voicemeeter type: '{kind_id}'")
|
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
|
value = KindId[kind_id.upper()].value
|
||||||
self.call(self.vm_runvm, value)
|
if BITS == 64 and self.bits == 64:
|
||||||
time.sleep(1)
|
value += 3
|
||||||
|
self.call(self.bind_run_voicemeeter, value)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
|
"""Returns the type of Voicemeeter installation (basic, banana, potato)."""
|
||||||
type_ = ct.c_long()
|
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()
|
return KindId(type_.value).name.lower()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> str:
|
def version(self) -> str:
|
||||||
"""Returns Voicemeeter's version as a string"""
|
"""Returns Voicemeeter's version as a string"""
|
||||||
ver = ct.c_long()
|
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(
|
return "{}.{}.{}.{}".format(
|
||||||
(ver.value & 0xFF000000) >> 24,
|
(ver.value & 0xFF000000) >> 24,
|
||||||
(ver.value & 0x00FF0000) >> 16,
|
(ver.value & 0x00FF0000) >> 16,
|
||||||
@@ -105,18 +113,16 @@ class Remote(CBindings):
|
|||||||
@property
|
@property
|
||||||
def pdirty(self) -> bool:
|
def pdirty(self) -> bool:
|
||||||
"""True iff UI parameters have been updated."""
|
"""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
|
@property
|
||||||
def mdirty(self) -> bool:
|
def mdirty(self) -> bool:
|
||||||
"""True iff MB parameters have been updated."""
|
"""True iff MB parameters have been updated."""
|
||||||
try:
|
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:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise CAPIError(
|
raise CAPIError("VBVMR_MacroButton_IsDirty", -9) from e
|
||||||
"no bind for VBVMR_MacroButton_IsDirty. are you using an old version of the API?"
|
|
||||||
) from e
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ldirty(self) -> bool:
|
def ldirty(self) -> bool:
|
||||||
@@ -127,12 +133,14 @@ class Remote(CBindings):
|
|||||||
and self.cache.get("bus_level") == self._bus_buf
|
and self.cache.get("bus_level") == self._bus_buf
|
||||||
)
|
)
|
||||||
|
|
||||||
def clear_dirty(self) -> NoReturn:
|
def clear_dirty(self) -> None:
|
||||||
try:
|
try:
|
||||||
while self.pdirty or self.mdirty:
|
while self.pdirty or self.mdirty:
|
||||||
pass
|
pass
|
||||||
except CAPIError:
|
except CAPIError as e:
|
||||||
self.logger.error("no bind for mdirty, clearing pdirty only")
|
if not (e.fn_name == "VBVMR_MacroButton_IsDirty" and e.code == -9):
|
||||||
|
raise
|
||||||
|
self.logger.error(f"{e} clearing pdirty only.")
|
||||||
while self.pdirty:
|
while self.pdirty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -141,59 +149,62 @@ class Remote(CBindings):
|
|||||||
"""Gets a string or float parameter"""
|
"""Gets a string or float parameter"""
|
||||||
if is_string:
|
if is_string:
|
||||||
buf = ct.create_unicode_buffer(512)
|
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:
|
else:
|
||||||
buf = ct.c_float()
|
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
|
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"""
|
"""Sets a string or float parameter. Caches value"""
|
||||||
if isinstance(val, str):
|
if isinstance(val, str):
|
||||||
if len(val) >= 512:
|
if len(val) >= 512:
|
||||||
raise VMError("String is too long")
|
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:
|
else:
|
||||||
self.call(
|
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
|
self.cache[param] = val
|
||||||
|
|
||||||
@polling
|
@polling
|
||||||
def get_buttonstatus(self, id: int, mode: int) -> int:
|
def get_buttonstatus(self, id_: int, mode: int) -> int:
|
||||||
"""Gets a macrobutton parameter"""
|
"""Gets a macrobutton parameter"""
|
||||||
state = ct.c_float()
|
c_state = ct.c_float()
|
||||||
try:
|
try:
|
||||||
self.call(
|
self.call(
|
||||||
self.vm_get_buttonstatus,
|
self.bind_macro_button_get_status,
|
||||||
ct.c_long(id),
|
ct.c_long(id_),
|
||||||
ct.byref(state),
|
ct.byref(c_state),
|
||||||
ct.c_long(mode),
|
ct.c_long(mode),
|
||||||
)
|
)
|
||||||
except AttributeError as e:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise CAPIError(
|
raise CAPIError("VBVMR_MacroButton_GetStatus", -9) from e
|
||||||
"no bind for VBVMR_MacroButton_GetStatus. are you using an old version of the API?"
|
return int(c_state.value)
|
||||||
) from e
|
|
||||||
return int(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"""
|
"""Sets a macrobutton parameter. Caches value"""
|
||||||
c_state = ct.c_float(float(state))
|
c_state = ct.c_float(float(val))
|
||||||
try:
|
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:
|
except AttributeError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise CAPIError(
|
raise CAPIError("VBVMR_MacroButton_SetStatus", -9) from e
|
||||||
"no bind for VBVMR_MacroButton_SetStatus. are you using an old version of the API?"
|
self.cache[f"mb_{id_}_{mode}"] = int(c_state.value)
|
||||||
) from e
|
|
||||||
self.cache[f"mb_{id}_{mode}"] = int(c_state.value)
|
|
||||||
|
|
||||||
def get_num_devices(self, direction: str = None) -> int:
|
def get_num_devices(self, direction: str = None) -> int:
|
||||||
"""Retrieves number of physical devices connected"""
|
"""Retrieves number of physical devices connected"""
|
||||||
if direction not in ("in", "out"):
|
if direction not in ("in", "out"):
|
||||||
raise VMError("Expected a direction: in or 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)
|
res = self.call(func, ok_exp=lambda r: r >= 0)
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@@ -204,7 +215,7 @@ class Remote(CBindings):
|
|||||||
type_ = ct.c_long()
|
type_ = ct.c_long()
|
||||||
name = ct.create_unicode_buffer(256)
|
name = ct.create_unicode_buffer(256)
|
||||||
hwid = 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(
|
self.call(
|
||||||
func,
|
func,
|
||||||
ct.c_long(index),
|
ct.c_long(index),
|
||||||
@@ -217,7 +228,9 @@ class Remote(CBindings):
|
|||||||
def get_level(self, type_: int, index: int) -> float:
|
def get_level(self, type_: int, index: int) -> float:
|
||||||
"""Retrieves a single level value"""
|
"""Retrieves a single level value"""
|
||||||
val = ct.c_float()
|
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
|
return val.value
|
||||||
|
|
||||||
def _get_levels(self) -> Iterable:
|
def _get_levels(self) -> Iterable:
|
||||||
@@ -227,18 +240,21 @@ class Remote(CBindings):
|
|||||||
return (
|
return (
|
||||||
tuple(
|
tuple(
|
||||||
self.get_level(self.strip_mode, i)
|
self.get_level(self.strip_mode, i)
|
||||||
for i in range(2 * self.kind.phys_in + 8 * self.kind.virt_in)
|
for i in range(self.kind.num_strip_levels)
|
||||||
),
|
|
||||||
tuple(
|
|
||||||
self.get_level(3, i)
|
|
||||||
for i in range(8 * (self.kind.phys_out + self.kind.virt_out))
|
|
||||||
),
|
),
|
||||||
|
tuple(self.get_level(3, i) for i in range(self.kind.num_bus_levels)),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_midi_message(self):
|
def get_midi_message(self):
|
||||||
n = ct.c_long(1024)
|
n = ct.c_long(1024)
|
||||||
buf = ct.create_string_buffer(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:
|
if res > 0:
|
||||||
vals = tuple(
|
vals = tuple(
|
||||||
grouper(3, (int.from_bytes(buf[i], "little") for i in range(res)))
|
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"""
|
"""Sets many parameters from a script"""
|
||||||
if len(script) > 48000:
|
if len(script) > 48000:
|
||||||
raise ValueError("Script too large, max size 48kB")
|
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)
|
time.sleep(self.DELAY * 5)
|
||||||
|
|
||||||
def apply(self, data: dict):
|
def apply(self, data: dict):
|
||||||
@@ -266,41 +282,64 @@ class Remote(CBindings):
|
|||||||
minor delay between each recursion
|
minor delay between each recursion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def param(key):
|
def target(key):
|
||||||
obj, m2, *rem = key.split("-")
|
match key.split("-"):
|
||||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
case ["strip" | "bus" | "button" as kls, index] if index.isnumeric():
|
||||||
if obj in ("strip", "bus", "button"):
|
target = getattr(self, kls)
|
||||||
return getattr(self, obj)[index]
|
case [
|
||||||
elif obj == "vban":
|
"vban",
|
||||||
return getattr(getattr(self, obj), f"{m2}stream")[index]
|
"in" | "instream" | "out" | "outstream" as direction,
|
||||||
raise ValueError(obj)
|
index,
|
||||||
|
] if index.isnumeric():
|
||||||
|
target = getattr(
|
||||||
|
self.vban, f"{direction.removesuffix('stream')}stream"
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
ERR_MSG = f"invalid config key '{key}'"
|
||||||
|
self.logger.error(ERR_MSG)
|
||||||
|
raise ValueError(ERR_MSG)
|
||||||
|
return target[int(index)]
|
||||||
|
|
||||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
[target(key).apply(di).then_wait() for key, di in data.items()]
|
||||||
|
|
||||||
def apply_config(self, name):
|
def apply_config(self, name):
|
||||||
"""applies a config from memory"""
|
"""applies a config from memory"""
|
||||||
error_msg = (
|
ERR_MSG = (
|
||||||
f"No config with name '{name}' is loaded into memory",
|
f"No config with name '{name}' is loaded into memory",
|
||||||
f"Known configs: {list(self.configs.keys())}",
|
f"Known configs: {list(self.configs.keys())}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.apply(self.configs[name])
|
config = self.configs[name]
|
||||||
self.logger.info(f"Profile '{name}' applied!")
|
except KeyError as e:
|
||||||
except KeyError:
|
self.logger.error(("\n").join(ERR_MSG))
|
||||||
self.logger.error(("\n").join(error_msg))
|
raise VMError(("\n").join(ERR_MSG)) from e
|
||||||
|
|
||||||
def logout(self) -> NoReturn:
|
if "extends" in config:
|
||||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
extended = config["extends"]
|
||||||
self.clear_dirty()
|
config = {
|
||||||
time.sleep(0.1)
|
k: v
|
||||||
self.call(self.vm_logout)
|
for k, v in deep_merge(self.configs[extended], config)
|
||||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
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):
|
def end_thread(self):
|
||||||
|
if not self.stopped():
|
||||||
self.logger.debug("events thread shutdown started")
|
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"""
|
"""teardown procedures"""
|
||||||
self.end_thread()
|
self.end_thread()
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ class PhysicalStrip(Strip):
|
|||||||
"""
|
"""
|
||||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||||
return type(
|
return type(
|
||||||
f"PhysicalStrip",
|
"PhysicalStrip",
|
||||||
(cls, EFFECTS_cls),
|
(cls, EFFECTS_cls),
|
||||||
{
|
{
|
||||||
"comp": StripComp(remote, i),
|
"comp": StripComp(remote, i),
|
||||||
@@ -168,7 +168,7 @@ class StripComp(IRemote):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def knee(self) -> float:
|
def knee(self) -> float:
|
||||||
return round(self.getter("Knee"), 1)
|
return round(self.getter("Knee"), 2)
|
||||||
|
|
||||||
@knee.setter
|
@knee.setter
|
||||||
def knee(self, val: float):
|
def knee(self, val: float):
|
||||||
@@ -337,7 +337,7 @@ class VirtualStrip(Strip):
|
|||||||
"""
|
"""
|
||||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||||
return type(
|
return type(
|
||||||
f"VirtualStrip",
|
"VirtualStrip",
|
||||||
(cls, EFFECTS_cls),
|
(cls, EFFECTS_cls),
|
||||||
{},
|
{},
|
||||||
)
|
)
|
||||||
@@ -364,7 +364,7 @@ class VirtualStrip(Strip):
|
|||||||
self.setter("karaoke", val)
|
self.setter("karaoke", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bass(self):
|
def bass(self) -> float:
|
||||||
return round(self.getter("EQGain1"), 1)
|
return round(self.getter("EQGain1"), 1)
|
||||||
|
|
||||||
@bass.setter
|
@bass.setter
|
||||||
@@ -372,7 +372,7 @@ class VirtualStrip(Strip):
|
|||||||
self.setter("EQGain1", val)
|
self.setter("EQGain1", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def mid(self):
|
def mid(self) -> float:
|
||||||
return round(self.getter("EQGain2"), 1)
|
return round(self.getter("EQGain2"), 1)
|
||||||
|
|
||||||
@mid.setter
|
@mid.setter
|
||||||
@@ -382,7 +382,7 @@ class VirtualStrip(Strip):
|
|||||||
med = mid
|
med = mid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def treble(self):
|
def treble(self) -> float:
|
||||||
return round(self.getter("EQGain3"), 1)
|
return round(self.getter("EQGain3"), 1)
|
||||||
|
|
||||||
high = treble
|
high = treble
|
||||||
@@ -415,7 +415,7 @@ class StripLevel(IRemote):
|
|||||||
def fget(x):
|
def fget(x):
|
||||||
return round(20 * log(x, 10), 1) if x > 0 else -200.0
|
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]]
|
vals = self._remote.cache["strip_level"][self.range[0] : self.range[-1]]
|
||||||
else:
|
else:
|
||||||
vals = [self._remote.get_level(mode, i) for i in range(*self.range)]
|
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.
|
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]])
|
return any(self._remote._strip_comp[self.range[0] : self.range[-1]])
|
||||||
|
|
||||||
is_updated = isdirty
|
is_updated = isdirty
|
||||||
@@ -491,7 +491,7 @@ class GainLayer(IRemote):
|
|||||||
def _make_gainlayer_mixin(remote, index):
|
def _make_gainlayer_mixin(remote, index):
|
||||||
"""Creates a GainLayer mixin"""
|
"""Creates a GainLayer mixin"""
|
||||||
return type(
|
return type(
|
||||||
f"GainlayerMixin",
|
"GainlayerMixin",
|
||||||
(),
|
(),
|
||||||
{
|
{
|
||||||
"gainlayer": tuple(
|
"gainlayer": tuple(
|
||||||
|
|||||||
@@ -10,14 +10,18 @@ logger = logging.getLogger(__name__)
|
|||||||
class Producer(threading.Thread):
|
class Producer(threading.Thread):
|
||||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||||
|
|
||||||
def __init__(self, remote, queue):
|
def __init__(self, remote, queue, stop_event):
|
||||||
super().__init__(name="producer", daemon=True)
|
super().__init__(name="producer", daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
|
self.stop_event = stop_event
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._remote.running:
|
while not self.stopped():
|
||||||
if self._remote.event.pdirty:
|
if self._remote.event.pdirty:
|
||||||
self.queue.put("pdirty")
|
self.queue.put("pdirty")
|
||||||
if self._remote.event.mdirty:
|
if self._remote.event.mdirty:
|
||||||
@@ -36,10 +40,8 @@ class Updater(threading.Thread):
|
|||||||
super().__init__(name="updater", daemon=True)
|
super().__init__(name="updater", daemon=True)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self._remote._strip_comp = [False] * (
|
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||||
2 * self._remote.kind.phys_in + 8 * self._remote.kind.virt_in
|
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||||
)
|
|
||||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
|
||||||
(
|
(
|
||||||
self._remote.cache["strip_level"],
|
self._remote.cache["strip_level"],
|
||||||
self._remote.cache["bus_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.
|
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||||
"""
|
"""
|
||||||
while True:
|
while event := self.queue.get():
|
||||||
event = self.queue.get()
|
|
||||||
if event is None:
|
|
||||||
self.logger.debug(f"terminating {self.name} thread")
|
|
||||||
break
|
|
||||||
|
|
||||||
if event == "pdirty" and self._remote.pdirty:
|
if event == "pdirty" and self._remote.pdirty:
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
elif event == "mdirty" and self._remote.mdirty:
|
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["strip_level"] = self._remote._strip_buf
|
||||||
self._remote.cache["bus_level"] = self._remote._bus_buf
|
self._remote.cache["bus_level"] = self._remote._bus_buf
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
|
|||||||
@@ -1,7 +1,41 @@
|
|||||||
import functools
|
import functools
|
||||||
|
import time
|
||||||
from itertools import zip_longest
|
from itertools import zip_longest
|
||||||
from typing import Iterator
|
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):
|
def polling(func):
|
||||||
"""
|
"""
|
||||||
@@ -70,3 +104,17 @@ def grouper(n, iterable, fillvalue=None):
|
|||||||
"""
|
"""
|
||||||
args = [iter(iterable)] * n
|
args = [iter(iterable)] * n
|
||||||
return zip_longest(fillvalue=fillvalue, *args)
|
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 abc import abstractmethod
|
||||||
|
|
||||||
from .error import VMError
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
|
from .kinds import kinds_all
|
||||||
|
|
||||||
|
|
||||||
class VbanStream(IRemote):
|
class VbanStream(IRemote):
|
||||||
@@ -50,7 +50,9 @@ class VbanStream(IRemote):
|
|||||||
@port.setter
|
@port.setter
|
||||||
def port(self, val: int):
|
def port(self, val: int):
|
||||||
if not 1024 <= val <= 65535:
|
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)
|
self.setter("port", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -61,7 +63,7 @@ class VbanStream(IRemote):
|
|||||||
def sr(self, val: int):
|
def sr(self, val: int):
|
||||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||||
if val not in opts:
|
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)
|
self.setter("sr", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -71,7 +73,7 @@ class VbanStream(IRemote):
|
|||||||
@channel.setter
|
@channel.setter
|
||||||
def channel(self, val: int):
|
def channel(self, val: int):
|
||||||
if not 1 <= val <= 8:
|
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)
|
self.setter("channel", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -81,7 +83,7 @@ class VbanStream(IRemote):
|
|||||||
@bit.setter
|
@bit.setter
|
||||||
def bit(self, val: int):
|
def bit(self, val: int):
|
||||||
if val not in (16, 24):
|
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)
|
self.setter("bit", 1 if (val == 16) else 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -91,7 +93,7 @@ class VbanStream(IRemote):
|
|||||||
@quality.setter
|
@quality.setter
|
||||||
def quality(self, val: int):
|
def quality(self, val: int):
|
||||||
if not 0 <= val <= 4:
|
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)
|
self.setter("quality", val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -101,7 +103,7 @@ class VbanStream(IRemote):
|
|||||||
@route.setter
|
@route.setter
|
||||||
def route(self, val: int):
|
def route(self, val: int):
|
||||||
if not 0 <= val <= 8:
|
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)
|
self.setter("route", val)
|
||||||
|
|
||||||
|
|
||||||
@@ -132,6 +134,18 @@ class VbanInstream(VbanStream):
|
|||||||
return super(VbanInstream, self).bit
|
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 VbanOutstream(VbanStream):
|
||||||
"""
|
"""
|
||||||
class representing a vban outstream
|
class representing a vban outstream
|
||||||
@@ -147,6 +161,42 @@ class VbanOutstream(VbanStream):
|
|||||||
return "out"
|
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 Vban:
|
||||||
"""
|
"""
|
||||||
class representing the vban module
|
class representing the vban module
|
||||||
@@ -156,9 +206,7 @@ class Vban:
|
|||||||
|
|
||||||
def __init__(self, remote):
|
def __init__(self, remote):
|
||||||
self.remote = remote
|
self.remote = remote
|
||||||
num_instream, num_outstream = remote.kind.vban
|
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||||
self.instream = tuple(VbanInstream(remote, i) for i in range(num_instream))
|
|
||||||
self.outstream = tuple(VbanOutstream(remote, i) for i in range(num_outstream))
|
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
self.remote.set("vban.Enable", 1)
|
self.remote.set("vban.Enable", 1)
|
||||||
|
|||||||
Reference in New Issue
Block a user