mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-07 08:03:30 +00:00
Compare commits
132 Commits
main
...
afa1867abc
| Author | SHA1 | Date | |
|---|---|---|---|
| afa1867abc | |||
| fcb656b7d0 | |||
| 9c0e2bef39 | |||
| 36692d1bc7 | |||
| 753714b639 | |||
| 27a26b8fe9 | |||
| 79260a0e47 | |||
| f9bcbfa74a | |||
| 0f2fb7121d | |||
| a635109308 | |||
| a61e09b075 | |||
| 763e44df12 | |||
| 69472a783e | |||
| 9a1ba06a21 | |||
| 14b2ee473a | |||
| ca2427c29a | |||
| ebacdcf82a | |||
| 7416108489 | |||
| bd6e57b3c6 | |||
| eed036ca03 | |||
| 55211b9b19 | |||
| 4af7c0f694 | |||
| f082fa8ac5 | |||
| cbcca14481 | |||
| f584d53835 | |||
| 72d182a488 | |||
| ee32f92914 | |||
| 3b65035e50 | |||
| c8b4bde49d | |||
| 47e9203b1e | |||
| d48e7ecd79 | |||
| 7e09a0d321 | |||
| d41ee1a12a | |||
| 1e499cd99d | |||
| 9bf52b5c11 | |||
| 77ba347e99 | |||
| 94fa33cebf | |||
| ef105d878b | |||
| 956f759e73 | |||
| dab519be9f | |||
| a4b91bf5c6 | |||
| 2a98707bf8 | |||
| 8e30c57020 | |||
| 04e18b304b | |||
| 4de384c66c | |||
| 2c8659a4e5 | |||
| 41e427e46b | |||
| fc6fdb44b5 | |||
| b49dc3b9b3 | |||
| 1ad0347478 | |||
| 2c8e4cc87c | |||
| fc3b31dfa7 | |||
| 544e0f2a32 | |||
| f6d92d1c34 | |||
| 10dbf63056 | |||
| 6ddd4151b4 | |||
| 8b912a2d08 | |||
| d2a5fe197e | |||
| 0970bfe0b5 | |||
| 54041503c9 | |||
| 9d015755eb | |||
| ca9a31c94a | |||
| 7a3abfc372 | |||
| 37a9c88867 | |||
| df7996a846 | |||
| 3f5dc7c376 | |||
| 05cbc432b2 | |||
| 174d95d08d | |||
| fc324fecc4 | |||
| 449cb9b3c1 | |||
| cdccc603d1 | |||
| a8bb9711af | |||
| 5bb0c2731e | |||
| 372dba0b6b | |||
| 226fc5ead7 | |||
| 9196a4e267 | |||
| 8485992495 | |||
| 91e49cbb55 | |||
| 3c85903554 | |||
| a730edc2c2 | |||
| 90acafe95b | |||
| 5f4fdcb0eb | |||
| d5219d66f7 | |||
| c74d827154 | |||
|
|
f6218d2032 | ||
|
|
4aacc60857 | ||
|
|
8f9ac47d02 | ||
|
|
90e994c193 | ||
|
|
44cd13aa48 | ||
|
|
87eb61170e | ||
|
|
01c99d5b31 | ||
|
|
3144a95e07 | ||
|
|
1833b28c8d | ||
|
|
ee3a871d23 | ||
|
|
197f81aa73 | ||
|
|
362873c5be | ||
|
|
c86f7971b0 | ||
|
|
bac60e5ed3 | ||
|
|
692acc8dd0 | ||
|
|
d57269f147 | ||
|
|
be69d905c4 | ||
|
|
5ceb8f775a | ||
|
|
e0f4aab257 | ||
|
|
4ee37f54c5 | ||
|
|
550df917fb | ||
|
|
2f82e0b1fc | ||
|
|
0c60fe3d5e | ||
|
|
243a43ac22 | ||
|
|
49354d6d55 | ||
|
|
5c9ac4d78f | ||
|
|
02b21b6989 | ||
|
|
4659cf7cdb | ||
|
|
8663aab2ce | ||
|
|
a029011012 | ||
|
|
bfa1a718f9 | ||
|
|
2048a807d1 | ||
|
|
566bff3ced | ||
|
|
70dbee6f02 | ||
|
|
c14196fc31 | ||
|
|
c28398c5f6 | ||
|
|
5177c2d297 | ||
|
|
23bc15e437 | ||
|
|
db96872965 | ||
|
|
1169435104 | ||
|
|
f46abedf12 | ||
|
|
733fab45b4 | ||
|
|
444f95a9d6 | ||
|
|
14e538dca6 | ||
|
|
af5e81c339 | ||
|
|
aadfbd3925 | ||
|
|
4ef3d1f225 | ||
|
|
aea2be624e |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
# quick test
|
||||
quick.py
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -88,7 +85,7 @@ ipython_config.py
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
@@ -153,3 +150,12 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# quick test
|
||||
quick.py
|
||||
|
||||
#config
|
||||
config.toml
|
||||
vban.toml
|
||||
|
||||
.vscode/
|
||||
143
CHANGELOG.md
143
CHANGELOG.md
@@ -11,6 +11,149 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.4.9] - 2023-08-13
|
||||
|
||||
### Added
|
||||
|
||||
- Error tests added in tests/test_errors.py
|
||||
- Errors section in README updated.
|
||||
|
||||
### Changed
|
||||
|
||||
- VBANCMDConnectionError class now subclasses VBANCMDError
|
||||
- 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-08
|
||||
|
||||
### Added
|
||||
|
||||
- button, vban classes implemented
|
||||
- \__repr\__() method added to base class
|
||||
|
||||
## [2.1.2] - 2023-07-05
|
||||
|
||||
### Added
|
||||
|
||||
- `outbound` kwarg let's you disable incoming rt packets. Essentially the interface will work only in one direction.
|
||||
|
||||
This is useful if you are only interested in sending commands out to voicemeeter but don't need to receive parameter states.
|
||||
|
||||
By default outbound is False.
|
||||
|
||||
- sendtext logging added in base class.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Bug in apply() if invoked from a higher class (not base class)
|
||||
|
||||
## [2.0.0] - 2023-06-25
|
||||
|
||||
This update introduces some breaking changes:
|
||||
|
||||
### Changed
|
||||
|
||||
- `strip[i].comp` now references StripComp class
|
||||
- To change the comp knob you should now use the property `strip[i].comp.knob`
|
||||
- `strip[i].gate` now references StripGate class
|
||||
|
||||
- To change the gate knob you should now use the property `strip[i].gate.knob`
|
||||
|
||||
- `bus[i].eq` now references BusEQ class
|
||||
|
||||
- To set bus[i].{eq,eq_ab} as before you should now use bus[i].eq.on and bus[i].eq.ab
|
||||
|
||||
- new error class `VBANCMDConnectionError` raised when a connection fails or times out.
|
||||
|
||||
There are other non-breaking changes:
|
||||
|
||||
### Changed
|
||||
|
||||
- now using a producer thread to send events to the updater thread.
|
||||
- factory.request_vbancmd_obj simply raises a `VBANCMDError` if passed an incorrect kind.
|
||||
- module level loggers implemented (with class loggers as child loggers)
|
||||
|
||||
### Added
|
||||
|
||||
- `strip[i].eq` added to PhysicalStrip
|
||||
|
||||
## [1.8.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Connection section to README.
|
||||
|
||||
### Changed
|
||||
|
||||
- now using clear_dirty() when sync enabled.
|
||||
|
||||
### Fixed
|
||||
|
||||
- bug in set_rt() where multiple commands sent in single request packet.
|
||||
- bug in apply where index was sent twice.
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
### Added
|
||||
|
||||
- ability to read conn info from vban.toml config
|
||||
|
||||
### Changed
|
||||
|
||||
- assume a vban.toml in examples. README's modified.
|
||||
|
||||
## [1.6.0] - 2022-10-06
|
||||
|
||||
### Added
|
||||
|
||||
- fadeto(), fadeby() methods added to strip/bus classes.
|
||||
- OBS example added.
|
||||
|
||||
### Changed
|
||||
|
||||
- Event class add/remove now accept iterables.
|
||||
- property setters added to Event class.
|
||||
- ldirty logic moved into VbanRtPacket class.
|
||||
- in util, threshold a level is considered dirty moved to 7200 (-72.0)
|
||||
- now print bus levels in observer example.
|
||||
|
||||
### Fixed
|
||||
|
||||
- initialize comps in updater thread. fixes bug when switching to a kind before any level updates
|
||||
|
||||
## [1.5.0] - 2022-09-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Logging module used in place of print statements across the interface.
|
||||
- base error name changed (VBANCMDError)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Timeout and raise connection error when socket connection fails.
|
||||
- Bug in observer example
|
||||
|
||||
## [1.4.0] - 2022-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- tomli/tomllib compatibility layer to support python 3.10
|
||||
|
||||
## [1.3.0] - 2022-08-02
|
||||
|
||||
### Added
|
||||
|
||||
357
README.md
357
README.md
@@ -1,5 +1,6 @@
|
||||
[](https://badge.fury.io/py/vban-cmd)
|
||||
[](https://github.com/onyx-and-iris/vban-cmd-python/blob/dev/LICENSE)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://pycqa.github.io/isort/)
|
||||

|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
# VBAN CMD
|
||||
|
||||
This python interface allows you to get and set Voicemeeter parameter values over a network.
|
||||
This python interface allows you to transmit Voicemeeter parameters over a network.
|
||||
|
||||
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
|
||||
|
||||
@@ -18,31 +19,42 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## Tested against
|
||||
|
||||
- Basic 1.0.8.4
|
||||
- Banana 2.0.6.4
|
||||
- Potato 3.0.2.4
|
||||
- Basic 1.0.8.8
|
||||
- Banana 2.0.6.8
|
||||
- Potato 3.0.2.8
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Voicemeeter](https://voicemeeter.com/)
|
||||
- Python 3.11 or greater
|
||||
- Python 3.10 or greater
|
||||
|
||||
## Installation
|
||||
|
||||
### `Pip`
|
||||
|
||||
Install vban-cmd package from your console
|
||||
|
||||
`pip install vban-cmd`
|
||||
|
||||
## `Use`
|
||||
|
||||
#### Connection
|
||||
|
||||
Load VBAN connection info from toml config. A valid `vban.toml` might look like this:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
It should be placed in \<user home directory\> / "Documents" / "Voicemeeter" / "configs"
|
||||
|
||||
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
||||
|
||||
#### `__main__.py`
|
||||
|
||||
Simplest use case, use a context manager to request a VbanCmd class of a kind.
|
||||
|
||||
Login and logout are handled for you in this scenario.
|
||||
|
||||
#### `__main__.py`
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
|
||||
@@ -59,17 +71,21 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq.on = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq.on}",
|
||||
)
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
KIND_ID = "banana"
|
||||
|
||||
with vban_cmd.api(
|
||||
KIND_ID, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
) as vban:
|
||||
do = ManyThings(vban)
|
||||
do.things()
|
||||
do.other_things()
|
||||
@@ -78,27 +94,21 @@ def main():
|
||||
vban.apply(
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"bus-2": {"mute": True, "eq": {"on": True}},
|
||||
"vban-in-0": {"on": True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
Otherwise you must remember to call `vban.login()`, `vban.logout()` at the start/end of your code.
|
||||
|
||||
## `kind_id`
|
||||
## `KIND_ID`
|
||||
|
||||
Pass the kind of Voicemeeter as an argument. kind_id may be:
|
||||
Pass the kind of Voicemeeter as an argument. KIND_ID may be:
|
||||
|
||||
- `basic`
|
||||
- `banana`
|
||||
@@ -116,17 +126,95 @@ The following properties are available.
|
||||
- `label`: string
|
||||
- `gain`: float, -60 to 12
|
||||
- `A1 - A5`, `B1 - B3`: boolean
|
||||
- `comp`: float, from 0.0 to 10.0
|
||||
- `gate`: float, from 0.0 to 10.0
|
||||
- `limit`: int, from -40 to 12
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[3].gain = 3.7
|
||||
print(strip[0].label)
|
||||
print(vban.strip[0].label)
|
||||
```
|
||||
|
||||
The following methods are available.
|
||||
|
||||
- `appgain(name, value)`: string, float, from 0.0 to 1.0
|
||||
|
||||
Set the gain in db by value for the app matching name.
|
||||
|
||||
- `appmute(name, value)`: string, bool
|
||||
|
||||
Set mute state as value for the app matching name.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[5].appmute("Spotify", True)
|
||||
vban.strip[5].appgain("Spotify", 0.5)
|
||||
```
|
||||
|
||||
##### Strip.Comp
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
- `gainin`: float, from -24.0 to 24.0
|
||||
- `ratio`: float, from 1.0 to 8.0
|
||||
- `threshold`: float, from -40.0 to -3.0
|
||||
- `attack`: float, from 0.0 to 200.0
|
||||
- `release`: float, from 0.0 to 5000.0
|
||||
- `knee`: float, from 0.0 to 1.0
|
||||
- `gainout`: float, from -24.0 to 24.0
|
||||
- `makeup`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
print(vban.strip[4].comp.knob)
|
||||
```
|
||||
|
||||
Strip Comp properties are defined as write only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Gate
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
- `threshold`: float, from -60.0 to -10.0
|
||||
- `damping`: float, from -60.0 to -10.0
|
||||
- `bpsidechain`: int, from 100 to 4000
|
||||
- `attack`: float, from 0.0 to 1000.0
|
||||
- `hold`: float, from 0.0 to 5000.0
|
||||
- `release`: float, from 0.0 to 5000.0
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[2].gate.attack = 300.8
|
||||
```
|
||||
|
||||
Strip Gate properties are defined as write only, potato version only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Denoiser
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
|
||||
strip.denoiser properties are defined as write only, potato version only.
|
||||
|
||||
##### Strip.EQ
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `ab`: boolean
|
||||
|
||||
Strip EQ properties are defined as write only, potato version only.
|
||||
|
||||
##### Gainlayers
|
||||
|
||||
- `gain`: float, from -60.0 to 12.0
|
||||
@@ -158,8 +246,6 @@ Level properties will return -200.0 if no audio detected.
|
||||
The following properties are available.
|
||||
|
||||
- `mono`: boolean
|
||||
- `eq`: boolean
|
||||
- `eq_ab`: boolean
|
||||
- `mute`: boolean
|
||||
- `label`: string
|
||||
- `gain`: float, -60 to 12
|
||||
@@ -167,10 +253,20 @@ The following properties are available.
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.bus[4].eq = true
|
||||
print(vban.bus[0].label)
|
||||
```
|
||||
|
||||
##### Bus.EQ
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `ab`: boolean
|
||||
|
||||
```python
|
||||
vban.bus[4].eq.on = true
|
||||
```
|
||||
|
||||
##### Modes
|
||||
|
||||
The following properties are available.
|
||||
@@ -213,6 +309,22 @@ print(vban.bus[0].levels.all)
|
||||
|
||||
`levels.all` will return -200.0 if no audio detected.
|
||||
|
||||
### Strip | Bus
|
||||
|
||||
The following methods are available.
|
||||
|
||||
- `fadeto(amount, time)`: float, int
|
||||
- `fadeby(amount, time)`: float, int
|
||||
|
||||
Modify gain to or by the selected amount in db over a time interval in ms.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].fadeto(-10.3, 1000)
|
||||
vban.bus[3].fadeby(-5.6, 500)
|
||||
```
|
||||
|
||||
### Command
|
||||
|
||||
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:
|
||||
@@ -242,8 +354,10 @@ vban.command.showvbanchat = true
|
||||
```python
|
||||
vban.apply(
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-1": {"mute": True, "mode": "composite"},
|
||||
"bus-2": {"eq": {"on": True}},
|
||||
"vban-in-0": {"on": True},
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -251,8 +365,8 @@ vban.apply(
|
||||
Or for each class you may do:
|
||||
|
||||
```python
|
||||
vban.strip[0].apply(mute: true, gain: 3.2, A1: true)
|
||||
vban.vban.outstream[0].apply(on: true, name: 'streamname', bit: 24)
|
||||
vban.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||
vban.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||
```
|
||||
|
||||
## Config Files
|
||||
@@ -261,7 +375,7 @@ vban.vban.outstream[0].apply(on: true, name: 'streamname', bit: 24)
|
||||
|
||||
You may load config files in TOML format.
|
||||
Three example configs have been included with the package. Remember to save
|
||||
current settings before loading a user config. To set one you may do:
|
||||
current settings before loading a user config. To load one you may do:
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
@@ -271,63 +385,110 @@ with vban_cmd.api('banana') as vban:
|
||||
|
||||
will load a config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||
|
||||
## `Base Module`
|
||||
Your configs may be located in one of the following paths:
|
||||
- \<current working directory\> / "configs" / kind_id
|
||||
- \<user home directory\> / ".config" / "vban-cmd" / kind_id
|
||||
- \<user home directory\> / "Documents" / "Voicemeeter" / "configs" / kind_id
|
||||
|
||||
### VbanCmd class
|
||||
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.
|
||||
|
||||
`vban_cmd.api(kind_id: str, **opts: dict)`
|
||||
#### `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
|
||||
|
||||
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
with vban_cmd.api('banana', ldirty=True, **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
#### `vban.subject`
|
||||
|
||||
Use the Subject class to register an app as event observer.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add`: registers an app as an event observer
|
||||
- `remove`: deregisters an app as an event observer
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
# register an app to receive updates
|
||||
class App():
|
||||
def __init__(self, vban):
|
||||
vban.subject.add(self)
|
||||
...
|
||||
```
|
||||
|
||||
#### `vban.event`
|
||||
|
||||
Use the event class to toggle updates as necessary.
|
||||
|
||||
The following properties are available:
|
||||
|
||||
- `pdirty`: boolean
|
||||
- `ldirty`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.ldirty = True
|
||||
|
||||
vban.event.pdirty = False
|
||||
```
|
||||
|
||||
Or add, remove a list of events.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add()`
|
||||
- `remove()`
|
||||
- `get()`
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.remove(["pdirty", "ldirty"])
|
||||
|
||||
# get a list of currently subscribed
|
||||
print(vban.event.get())
|
||||
```
|
||||
|
||||
## VbanCmd class
|
||||
|
||||
`vban_cmd.api(kind_id: str, **opts)`
|
||||
|
||||
You may pass the following optional keyword arguments:
|
||||
|
||||
- `ip`: str, ip or hostname of remote machine
|
||||
- `streamname`: str, name of the stream to connect to.
|
||||
- `port`: int=6980, vban udp port of remote machine.
|
||||
- `subs`: dict={"pdirty": True, "ldirty": False}, controls which updates to listen for.
|
||||
- `pdirty`: parameter updates
|
||||
- `ldirty`: level updates
|
||||
|
||||
#### Event updates
|
||||
|
||||
To receive event updates you should do the following:
|
||||
|
||||
- register your app to receive updates using the `vban.subject.add(observer)` method, where observer is your app.
|
||||
- define an `on_update(subject)` callback function in your app. The value of subject may be checked for the type of update.
|
||||
|
||||
See `examples/observer` for a demonstration.
|
||||
|
||||
Level updates are considered high volume, by default they are NOT listened for.
|
||||
|
||||
Each of the update types may be enabled/disabled separately.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
# Listen for level updates
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
"subs": {"ldirty": True},
|
||||
}
|
||||
with vban_cmd.api('banana', **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
#### `vban.event`
|
||||
|
||||
You may also add/remove event subscriptions as necessary with the Event class.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.add("ldirty")
|
||||
|
||||
vban.event.remove("pdirty")
|
||||
|
||||
# get a list of currently subscribed
|
||||
print(vban.event.get())
|
||||
```
|
||||
- `pdirty`: boolean=False, parameter updates
|
||||
- `ldirty`: boolean=False, level updates
|
||||
- `timeout`: int=5, amount of time (seconds) to wait for an incoming RT data packet (parameter states).
|
||||
- `outbound`: boolean=False, set `True` if you are only interested in sending commands. (no rt packets will be received)
|
||||
|
||||
#### `vban.pdirty`
|
||||
|
||||
@@ -347,13 +508,31 @@ vban.sendtext("Strip[0].Mute=1;Bus[0].Mono=1")
|
||||
|
||||
#### `vban.public_packet`
|
||||
|
||||
Returns a Voicemeeter rt data packet object. Designed to be used internally by the interface but available for parsing through this read only property object. States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||
Returns a `VbanRtPacket`. Designed to be used internally by the interface but available for parsing through this read only property object.
|
||||
|
||||
### `Errors`
|
||||
States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||
|
||||
- `errors.VMCMDErrors`: Base VMCMD error class.
|
||||
## Errors
|
||||
|
||||
### `Tests`
|
||||
- `errors.VBANCMDError`: Base VBANCMD Exception class.
|
||||
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
||||
|
||||
## Logging
|
||||
|
||||
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||
|
||||
example:
|
||||
```python
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
opts = {"ip": "ip.local", "port": 6980, "streamname": "Command1"}
|
||||
with vban_cmd.api('banana', **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
||||
|
||||
|
||||
19
__main__.py
19
__main__.py
@@ -13,17 +13,21 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
||||
)
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
kind_id = "banana"
|
||||
|
||||
with vban_cmd.api(
|
||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
) as vban:
|
||||
do = ManyThings(vban)
|
||||
do.things()
|
||||
do.other_things()
|
||||
@@ -33,19 +37,10 @@ def main():
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"button-0": {"state": True},
|
||||
"vban-in-0": {"on": True},
|
||||
"vban-out-1": {"name": "streamname"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
label = "PhysStrip0"
|
||||
A1 = true
|
||||
gain = -8.8
|
||||
comp = 3.2
|
||||
comp.knob = 3.2
|
||||
|
||||
[strip-1]
|
||||
label = "PhysStrip1"
|
||||
B1 = true
|
||||
gate = 4.1
|
||||
gate.knob = 4.1
|
||||
|
||||
[strip-2]
|
||||
label = "PhysStrip2"
|
||||
@@ -31,12 +31,12 @@ mono = true
|
||||
|
||||
[bus-2]
|
||||
label = "PhysBus2"
|
||||
eq = true
|
||||
eq.on = true
|
||||
mode = "composite"
|
||||
|
||||
[bus-3]
|
||||
label = "VirtBus0"
|
||||
eq_ab = true
|
||||
eq.ab = true
|
||||
mode = "upmix61"
|
||||
|
||||
[bus-4]
|
||||
|
||||
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"
|
||||
@@ -2,12 +2,12 @@
|
||||
label = "PhysStrip0"
|
||||
A1 = true
|
||||
gain = -8.8
|
||||
comp = 3.2
|
||||
comp.knob = 3.2
|
||||
|
||||
[strip-1]
|
||||
label = "PhysStrip1"
|
||||
B1 = true
|
||||
gate = 4.1
|
||||
gate.knob = 4.1
|
||||
|
||||
[strip-2]
|
||||
label = "PhysStrip2"
|
||||
@@ -47,7 +47,7 @@ mono = true
|
||||
|
||||
[bus-2]
|
||||
label = "PhysBus2"
|
||||
eq = true
|
||||
eq.on = true
|
||||
|
||||
[bus-3]
|
||||
label = "PhysBus3"
|
||||
@@ -59,7 +59,7 @@ mode = "composite"
|
||||
|
||||
[bus-5]
|
||||
label = "VirtBus0"
|
||||
eq_ab = true
|
||||
eq.ab = true
|
||||
|
||||
[bus-6]
|
||||
label = "VirtBus1"
|
||||
|
||||
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"
|
||||
13
examples/gui/README.md
Normal file
13
examples/gui/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## About
|
||||
|
||||
A single channel GUI demonstrating controls for the first virtual strip if Voicemeeter Banana.
|
||||
|
||||
This example demonstrates (to an extent) two way communication.
|
||||
- Sending parameters values to the Voicemeeter driver.
|
||||
- Receiving level updates
|
||||
|
||||
Parameter updates (pdirty) events are not being received so changing a UI element on the main Voicemeeter app will not be reflected in the example GUI.
|
||||
|
||||
## Use
|
||||
|
||||
Simply run the script and try the controls.
|
||||
109
examples/gui/__main__.py
Normal file
109
examples/gui/__main__.py
Normal file
@@ -0,0 +1,109 @@
|
||||
import logging
|
||||
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
|
||||
class App(tk.Tk):
|
||||
INDEX = 3
|
||||
|
||||
def __init__(self, vban):
|
||||
super().__init__()
|
||||
self.vban = vban
|
||||
self.title(f"{vban} - version {vban.version}")
|
||||
self.vban.observer.add(self.on_ldirty)
|
||||
|
||||
# create widget variables
|
||||
self.button_var = tk.BooleanVar(value=vban.strip[self.INDEX].mute)
|
||||
self.slider_var = tk.DoubleVar(value=vban.strip[self.INDEX].gain)
|
||||
self.meter_var = tk.DoubleVar(value=self._get_level())
|
||||
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
|
||||
|
||||
# initialize style table
|
||||
self.style = ttk.Style()
|
||||
self.style.theme_use("clam")
|
||||
self.style.configure(
|
||||
"Mute.TButton",
|
||||
foreground="#cd5c5c" if vban.strip[self.INDEX].mute else "#5a5a5a",
|
||||
)
|
||||
|
||||
# create labelframe and grid it onto the mainframe
|
||||
self.labelframe = tk.LabelFrame(text=self.vban.strip[self.INDEX].label)
|
||||
self.labelframe.grid(padx=1)
|
||||
|
||||
# create slider and grid it onto the labelframe
|
||||
slider = ttk.Scale(
|
||||
self.labelframe,
|
||||
from_=12,
|
||||
to_=-60,
|
||||
orient="vertical",
|
||||
variable=self.slider_var,
|
||||
command=lambda arg: self.on_slider_move(arg),
|
||||
)
|
||||
slider.grid(
|
||||
column=0,
|
||||
row=0,
|
||||
)
|
||||
slider.bind("<Double-Button-1>", self.on_button_double_click)
|
||||
|
||||
# create level meter and grid it onto the labelframe
|
||||
level_meter = ttk.Progressbar(
|
||||
self.labelframe,
|
||||
orient="vertical",
|
||||
variable=self.meter_var,
|
||||
maximum=72,
|
||||
mode="determinate",
|
||||
)
|
||||
level_meter.grid(column=1, row=0)
|
||||
|
||||
# create gainlabel and grid it onto the labelframe
|
||||
gainlabel = ttk.Label(self.labelframe, textvariable=self.gainlabel_var)
|
||||
gainlabel.grid(column=0, row=1, columnspan=2)
|
||||
|
||||
# create button and grid it onto the labelframe
|
||||
button = ttk.Button(
|
||||
self.labelframe,
|
||||
text="Mute",
|
||||
style="Mute.TButton",
|
||||
command=lambda: self.on_button_press(),
|
||||
)
|
||||
button.grid(column=0, row=2, columnspan=2, padx=1, pady=2)
|
||||
|
||||
# define callbacks
|
||||
|
||||
def on_slider_move(self, *args):
|
||||
val = round(self.slider_var.get(), 1)
|
||||
self.vban.strip[self.INDEX].gain = val
|
||||
self.gainlabel_var.set(val)
|
||||
|
||||
def on_button_press(self):
|
||||
self.button_var.set(not self.button_var.get())
|
||||
self.vban.strip[self.INDEX].mute = self.button_var.get()
|
||||
self.style.configure(
|
||||
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
||||
)
|
||||
|
||||
def on_button_double_click(self, e):
|
||||
self.slider_var.set(0)
|
||||
self.gainlabel_var.set(0)
|
||||
self.vban.strip[self.INDEX].gain = 0
|
||||
|
||||
def _get_level(self):
|
||||
val = max(self.vban.strip[self.INDEX].levels.prefader)
|
||||
return 0 if self.button_var.get() else 72 + val - 12 + self.slider_var.get()
|
||||
|
||||
def on_ldirty(self):
|
||||
self.meter_var.set(self._get_level())
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api("banana", ldirty=True) as vban:
|
||||
app = App(vban)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
53
examples/obs/README.md
Normal file
53
examples/obs/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
## Requirements
|
||||
|
||||
- [OBS Studio](https://obsproject.com/)
|
||||
- [OBS Python SDK for Websocket v5](https://github.com/aatikturk/obsws-python)
|
||||
|
||||
## About
|
||||
|
||||
Perhaps you have a streaming setup but you want to control OBS and Voicemeeter from a remote location with python installed.
|
||||
With the vban-cmd and obsws-python packages you may sync a distant Voicemeeter with a distant OBS over LAN.
|
||||
|
||||
## Configure
|
||||
|
||||
This script assumes the following:
|
||||
|
||||
- OBS Connection info in a valid `config.toml`:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
host = "gamepc.local"
|
||||
port = 4455
|
||||
password = "mystrongpass"
|
||||
```
|
||||
|
||||
- VBAN Connection info in a valid `vban.toml`:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
- Both configs should be placed next to `__main__.py`.
|
||||
|
||||
- Four OBS scenes named "START", "BRB", "END" and "LIVE".
|
||||
|
||||
## Use
|
||||
|
||||
Make sure you have established a working connection to OBS and the remote Voicemeeter.
|
||||
|
||||
Run the script, change OBS scenes and watch Voicemeeter parameters change.
|
||||
|
||||
Closing OBS will end the script.
|
||||
|
||||
## Notes
|
||||
|
||||
All but `vban_cmd.iremote` logs are filtered out. Log in DEBUG mode.
|
||||
|
||||
This script can be run from a Linux host since the vban-cmd interface relies on UDP packets and obsws-python runs over websockets.
|
||||
|
||||
You could for example, set this up to run in the background on a home server such as a Raspberry Pi.
|
||||
|
||||
It requires Python 3.10+.
|
||||
91
examples/obs/__main__.py
Normal file
91
examples/obs/__main__.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import time
|
||||
from logging import config
|
||||
|
||||
import obsws_python as obsws
|
||||
|
||||
import vban_cmd
|
||||
|
||||
config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"standard": {
|
||||
"format": "%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"stream": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "standard",
|
||||
}
|
||||
},
|
||||
"loggers": {"vban_cmd.iremote": {"handlers": ["stream"], "level": "DEBUG"}},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
self.client = obsws.EventClient()
|
||||
self.client.callback.register(
|
||||
(
|
||||
self.on_current_program_scene_changed,
|
||||
self.on_exit_started,
|
||||
)
|
||||
)
|
||||
self.is_running = True
|
||||
|
||||
def on_start(self):
|
||||
self.vban.strip[0].mute = True
|
||||
self.vban.strip[1].B1 = True
|
||||
self.vban.strip[2].B2 = True
|
||||
|
||||
def on_brb(self):
|
||||
self.vban.strip[7].fadeto(0, 500)
|
||||
self.vban.bus[0].mute = True
|
||||
|
||||
def on_end(self):
|
||||
self.vban.apply(
|
||||
{
|
||||
"strip-0": {"mute": True},
|
||||
"strip-1": {"mute": True, "B1": False},
|
||||
"strip-2": {"mute": True, "B1": False},
|
||||
}
|
||||
)
|
||||
|
||||
def on_live(self):
|
||||
self.vban.strip[0].mute = False
|
||||
self.vban.strip[7].fadeto(-6, 500)
|
||||
self.vban.strip[7].A3 = True
|
||||
|
||||
def on_current_program_scene_changed(self, data):
|
||||
def fget(scene):
|
||||
run = {
|
||||
"START": self.on_start,
|
||||
"BRB": self.on_brb,
|
||||
"END": self.on_end,
|
||||
"LIVE": self.on_live,
|
||||
}
|
||||
return run.get(scene)
|
||||
|
||||
scene = data.scene_name
|
||||
print(f"Switched to scene {scene}")
|
||||
if fn := fget(scene):
|
||||
fn()
|
||||
|
||||
def on_exit_started(self, _):
|
||||
self.client.unsubscribe()
|
||||
self.is_running = False
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api("potato") as vban:
|
||||
observer = Observer(vban)
|
||||
while observer.is_running:
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
7
examples/obs/setup.py
Normal file
7
examples/obs/setup.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="obs",
|
||||
description="OBS Example",
|
||||
install_requires=["obsws-python"],
|
||||
)
|
||||
29
examples/observer/README.md
Normal file
29
examples/observer/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## About
|
||||
|
||||
Registers a class as an observer and defines a callback.
|
||||
|
||||
## Configure
|
||||
|
||||
The script assumes you have connection info saved in a config file named `vban.toml` placed next to `__main__.py`.
|
||||
|
||||
A valid `vban.toml` might look like this:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
It should be placed next to `__main__.py`.
|
||||
|
||||
## Use
|
||||
|
||||
Make sure you have established a working VBAN connection.
|
||||
|
||||
Run the script, then:
|
||||
|
||||
- change GUI parameters to trigger pdirty
|
||||
- play audio through any bus to trigger ldirty
|
||||
|
||||
Pressing `<Enter>` will exit.
|
||||
@@ -1,47 +1,35 @@
|
||||
import logging
|
||||
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class Observer:
|
||||
|
||||
class App:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
# register your app as event observer
|
||||
self.vban.subject.add(self)
|
||||
# add level updates, since they are disabled by default.
|
||||
self.vm.event.add("ldirty")
|
||||
self.vban.observer.add(self)
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
def on_update(self, event):
|
||||
if event == "pdirty":
|
||||
print("pdirty!")
|
||||
elif subject == "ldirty":
|
||||
info = (
|
||||
f"[{self.vban.bus[0]} {self.vban.bus[0].levels.isdirty}]",
|
||||
f"[{self.vban.bus[1]} {self.vban.bus[1].levels.isdirty}]",
|
||||
f"[{self.vban.bus[2]} {self.vban.bus[2].levels.isdirty}]",
|
||||
f"[{self.vban.bus[3]} {self.vban.bus[3].levels.isdirty}]",
|
||||
f"[{self.vban.bus[4]} {self.vban.bus[4].levels.isdirty}]",
|
||||
f"[{self.vban.bus[5]} {self.vban.bus[5].levels.isdirty}]",
|
||||
f"[{self.vban.bus[6]} {self.vban.bus[6].levels.isdirty}]",
|
||||
f"[{self.vban.bus[7]} {self.vban.bus[7].levels.isdirty}]",
|
||||
)
|
||||
print(" ".join(info))
|
||||
elif event == "ldirty":
|
||||
for bus in self.vban.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
obs = Observer(vban)
|
||||
KIND_ID = "banana"
|
||||
|
||||
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True) as vban:
|
||||
App(vban)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "potato"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
236
poetry.lock
generated
236
poetry.lock
generated
@@ -1,11 +1,3 @@
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.1"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
@@ -22,7 +14,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.6.0"
|
||||
version = "22.8.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -33,6 +25,7 @@ click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
@@ -40,6 +33,22 @@ d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.3.1"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.1.0"
|
||||
description = "Universal encoding detector for Python 3"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
@@ -53,11 +62,31 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.6"
|
||||
description = "Distribution utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.12.2"
|
||||
description = "A platform independent file lock."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@@ -91,34 +120,31 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "3.7.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@@ -141,26 +167,30 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
name = "pyproject-api"
|
||||
version = "1.5.2"
|
||||
description = "API to interact with the python pyproject.toml based projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=23.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.2"
|
||||
version = "7.1.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
@@ -198,97 +228,77 @@ pytest = ">=3.6"
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.6.3"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=5.3.1"
|
||||
chardet = ">=5.1"
|
||||
colorama = ">=0.4.6"
|
||||
filelock = ">=3.12.2"
|
||||
packaging = ">=23.1"
|
||||
platformdirs = ">=3.5.3"
|
||||
pluggy = ">=1"
|
||||
pyproject-api = ">=1.5.2"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
virtualenv = ">=20.23.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.23.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.6,<1"
|
||||
filelock = ">=3.12,<4"
|
||||
platformdirs = ">=3.5.1,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = []
|
||||
attrs = []
|
||||
black = [
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
|
||||
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
|
||||
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
|
||||
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
|
||||
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
|
||||
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
|
||||
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
|
||||
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
|
||||
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
|
||||
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
|
||||
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
|
||||
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
|
||||
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
|
||||
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
|
||||
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
|
||||
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
|
||||
]
|
||||
pytest-randomly = [
|
||||
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
|
||||
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
|
||||
]
|
||||
black = []
|
||||
cachetools = []
|
||||
chardet = []
|
||||
click = []
|
||||
colorama = []
|
||||
distlib = []
|
||||
filelock = []
|
||||
iniconfig = []
|
||||
isort = []
|
||||
mypy-extensions = []
|
||||
packaging = []
|
||||
pathspec = []
|
||||
platformdirs = []
|
||||
pluggy = []
|
||||
py = []
|
||||
pyproject-api = []
|
||||
pytest = []
|
||||
pytest-randomly = []
|
||||
pytest-repeat = []
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
tomli = []
|
||||
tox = []
|
||||
virtualenv = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vban-cmd"
|
||||
version = "1.3.2"
|
||||
version = "2.4.9"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
@@ -8,7 +8,8 @@ readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/vban-cmd-python"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
python = "^3.10"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
@@ -17,7 +18,29 @@ pytest-randomly = "^3.12.0"
|
||||
pytest-repeat = "^0.9.1"
|
||||
black = "^22.3.0"
|
||||
isort = "^5.10.1"
|
||||
tox = "^4.6.3"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
gui = "scripts:ex_gui"
|
||||
obs = "scripts:ex_obs"
|
||||
observer = "scripts:ex_observer"
|
||||
basic = "scripts:test_basic"
|
||||
banana = "scripts:test_banana"
|
||||
potato = "scripts:test_potato"
|
||||
all = "scripts:test_all"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310,py311
|
||||
|
||||
[testenv]
|
||||
allowlist_externals = poetry
|
||||
commands =
|
||||
poetry install -v
|
||||
poetry run pytest tests/
|
||||
"""
|
||||
|
||||
39
scripts.py
Normal file
39
scripts.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_gui():
|
||||
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def test_basic():
|
||||
os.environ["KIND"] = "basic"
|
||||
subprocess.run(["tox"])
|
||||
|
||||
|
||||
def test_banana():
|
||||
os.environ["KIND"] = "banana"
|
||||
subprocess.run(["tox"])
|
||||
|
||||
|
||||
def test_potato():
|
||||
os.environ["KIND"] = "potato"
|
||||
subprocess.run(["tox"])
|
||||
|
||||
|
||||
def test_all():
|
||||
steps = [test_basic, test_banana, test_potato]
|
||||
[step() for step in steps]
|
||||
@@ -1,25 +1,26 @@
|
||||
import os
|
||||
import random
|
||||
import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
import vban_cmd
|
||||
from vban_cmd.kinds import KindId, kinds_all
|
||||
from vban_cmd.kinds import KindId
|
||||
from vban_cmd.kinds import request_kind_map as kindmap
|
||||
|
||||
# let's keep things random
|
||||
kind_id = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
# get KIND_ID from env var, otherwise set to random
|
||||
KIND_ID = os.environ.get(
|
||||
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
)
|
||||
|
||||
opts = {
|
||||
"ip": "ws.local",
|
||||
"streamname": "workstation",
|
||||
"ip": "testing.local",
|
||||
"streamname": "testing",
|
||||
"port": 6990,
|
||||
"bps": 0,
|
||||
"sync": True,
|
||||
}
|
||||
|
||||
vbans = {kind.name: vban_cmd.api(kind.name, **opts) for kind in kinds_all}
|
||||
tests = vbans[kind_id]
|
||||
kind = kindmap(kind_id)
|
||||
vban = vban_cmd.api(KIND_ID, **opts)
|
||||
kind = kindmap(KIND_ID)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -42,9 +43,9 @@ data = Data()
|
||||
|
||||
def setup_module():
|
||||
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
||||
tests.login()
|
||||
tests.command.reset()
|
||||
vban.login()
|
||||
vban.command.reset()
|
||||
|
||||
|
||||
def teardown_module():
|
||||
tests.logout()
|
||||
vban.logout()
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 46"><title>tests: 46</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">46</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">46</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 49"><title>tests: 49</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">49</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">49</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="60" height="20" role="img" aria-label="tests: 48"><title>tests: 48</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">48</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">48</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 51"><title>tests: 51</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">51</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">51</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="60" height="20" role="img" aria-label="tests: 52"><title>tests: 52</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">52</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">52</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="60" height="20" role="img" aria-label="tests: 59"><title>tests: 59</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="60" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="23" height="20" fill="#4c1"/><rect width="60" 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="475" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="130">59</text><text x="475" y="140" transform="scale(.1)" fill="#fff" textLength="130">59</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -11,7 +11,7 @@ Function RunTests {
|
||||
$line | Tee-Object -FilePath $coverage -Append
|
||||
}
|
||||
}
|
||||
Write-Output "$(Get-TimeStamp)" | Out-file $coverage -Append
|
||||
Write-Output "$(Get-TimeStamp)" | Out-File $coverage -Append
|
||||
|
||||
Invoke-Expression "genbadge tests -t 90 -i ./tests/.coverage.xml -o ./tests/$kind.svg"
|
||||
}
|
||||
@@ -25,7 +25,10 @@ Function Get-TimeStamp {
|
||||
if ($MyInvocation.InvocationName -ne ".") {
|
||||
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
||||
|
||||
@("potato") | ForEach-Object {
|
||||
$env:KIND = $_
|
||||
RunTests
|
||||
}
|
||||
|
||||
Invoke-Expression "deactivate"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import time
|
||||
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestSetAndGetBoolHigher:
|
||||
@@ -12,18 +12,27 @@ class TestSetAndGetBoolHigher:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
tests.apply_config("example")
|
||||
vban.apply_config("example")
|
||||
time.sleep(0.1)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
def test_it_tests_config_string(self):
|
||||
assert "PhysStrip" in tests.strip[data.phys_in].label
|
||||
assert "VirtStrip" in tests.strip[data.virt_in].label
|
||||
assert "PhysStrip" in vban.strip[data.phys_in].label
|
||||
assert "VirtStrip" in vban.strip[data.virt_in].label
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
def test_it_tests_config_bool(self):
|
||||
assert tests.strip[0].A1 == True
|
||||
assert vban.strip[0].A1 == True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
def test_it_tests_config_busmode(self):
|
||||
assert tests.bus[data.phys_out].mode.get() == "composite"
|
||||
assert vban.bus[data.phys_out].mode.get() == "composite"
|
||||
|
||||
37
tests/test_errors.py
Normal file
37
tests/test_errors.py
Normal file
@@ -0,0 +1,37 @@
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
import vban_cmd
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestErrors:
|
||||
__test__ = True
|
||||
|
||||
def test_it_tests_an_unknown_kind(self):
|
||||
with pytest.raises(
|
||||
vban_cmd.error.VBANCMDError,
|
||||
match=f"Unknown Voicemeeter kind 'unknown_kind'",
|
||||
):
|
||||
vban_cmd.api("unknown_kind")
|
||||
|
||||
def test_it_tests_an_unknown_config_name(self):
|
||||
EXPECTED_MSG = "\n".join(
|
||||
(
|
||||
f"No config with name 'unknown' is loaded into memory",
|
||||
f"Known configs: {list(vban.configs.keys())}",
|
||||
)
|
||||
)
|
||||
with pytest.raises(vban_cmd.error.VBANCMDError, match=re.escape(EXPECTED_MSG)):
|
||||
vban.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'"):
|
||||
vban.apply(CONFIG)
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestRemoteFactories:
|
||||
@@ -11,33 +11,45 @@ class TestRemoteFactories:
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_basic(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
assert hasattr(vban, "button")
|
||||
assert hasattr(vban, "vban")
|
||||
|
||||
assert len(tests.strip) == 3
|
||||
assert len(tests.bus) == 2
|
||||
assert len(vban.strip) == 3
|
||||
assert len(vban.bus) == 2
|
||||
assert len(vban.button) == 80
|
||||
assert len(vban.vban.instream) == 6 and len(vban.vban.outstream) == 5
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "banana",
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_banana(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
assert hasattr(vban, "button")
|
||||
assert hasattr(vban, "vban")
|
||||
|
||||
assert len(tests.strip) == 5
|
||||
assert len(tests.bus) == 5
|
||||
assert len(vban.strip) == 5
|
||||
assert len(vban.bus) == 5
|
||||
assert len(vban.button) == 80
|
||||
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_potato(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
assert hasattr(vban, "button")
|
||||
assert hasattr(vban, "vban")
|
||||
|
||||
assert len(tests.strip) == 8
|
||||
assert len(tests.bus) == 8
|
||||
assert len(vban.strip) == 8
|
||||
assert len(vban.bus) == 8
|
||||
assert len(vban.button) == 80
|
||||
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [False, True])
|
||||
@@ -17,8 +17,8 @@ class TestSetAndGetBoolHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "banana",
|
||||
@@ -31,23 +31,22 @@ class TestSetAndGetBoolHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params_mc(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param",
|
||||
[
|
||||
(data.phys_out, "eq"),
|
||||
(data.phys_out, "mute"),
|
||||
(data.virt_out, "eq_ab"),
|
||||
(data.virt_out, "sel"),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
assert hasattr(vban.bus[index], param)
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
||||
""" bus modes tests, physical and virtual """
|
||||
|
||||
@@ -66,8 +65,8 @@ class TestSetAndGetBoolHigher:
|
||||
# here it only makes sense to set/get bus modes as True
|
||||
if not value:
|
||||
value = True
|
||||
setattr(tests.bus[index].mode, param, value)
|
||||
assert getattr(tests.bus[index].mode, param) == value
|
||||
setattr(vban.bus[index].mode, param, value)
|
||||
assert getattr(vban.bus[index].mode, param) == value
|
||||
|
||||
""" command tests """
|
||||
|
||||
@@ -76,7 +75,7 @@ class TestSetAndGetBoolHigher:
|
||||
[("lock")],
|
||||
)
|
||||
def test_it_sets_command_bool_params(self, param, value):
|
||||
setattr(tests.command, param, value)
|
||||
setattr(vban.command, param, value)
|
||||
|
||||
|
||||
class TestSetAndGetIntHigher:
|
||||
@@ -94,8 +93,8 @@ class TestSetAndGetIntHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
|
||||
class TestSetAndGetFloatHigher:
|
||||
@@ -113,15 +112,15 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,value",
|
||||
[(data.phys_in, 2), (data.phys_in, 2), (data.virt_in, 8), (data.virt_in, 8)],
|
||||
)
|
||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||
assert len(tests.strip[index].levels.prefader) == value
|
||||
assert len(vban.strip[index].levels.prefader) == value
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
@@ -137,8 +136,42 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_gainlayer_values(self, index, j, value):
|
||||
tests.strip[index].gainlayer[j].gain = value
|
||||
assert tests.strip[index].gainlayer[j].gain == value
|
||||
vban.strip[index].gainlayer[j].gain = value
|
||||
assert vban.strip[index].gainlayer[j].gain == value
|
||||
|
||||
""" strip tests, physical """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Only test if logged into Potato version",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[
|
||||
(data.phys_in, "gainin", -8.6),
|
||||
(data.phys_in, "knee", 0.24),
|
||||
],
|
||||
)
|
||||
def test_it_sets_strip_comp_params(self, index, param, value):
|
||||
assert hasattr(vban.strip[index].comp, param)
|
||||
setattr(vban.strip[index].comp, param, value)
|
||||
# we can set but not get this value. Not in RT Packet.
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Only test if logged into Potato version",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[
|
||||
(data.phys_in, "bpsidechain", 120),
|
||||
(data.phys_in, "hold", 3000),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_gate_params(self, index, param, value):
|
||||
assert hasattr(vban.strip[index].gate, param)
|
||||
setattr(vban.strip[index].gate, param, value)
|
||||
# we can set but not get this value. Not in RT Packet.
|
||||
|
||||
""" strip tests, virtual """
|
||||
|
||||
@@ -151,8 +184,8 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@@ -161,15 +194,15 @@ class TestSetAndGetFloatHigher:
|
||||
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_float_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,value",
|
||||
[(data.phys_out, 8), (data.virt_out, 8)],
|
||||
)
|
||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||
assert len(tests.bus[index].levels.all) == value
|
||||
assert len(vban.bus[index].levels.all) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["test0", "test1"])
|
||||
@@ -183,8 +216,8 @@ class TestSetAndGetStringHigher:
|
||||
[(data.phys_in, "label"), (data.virt_in, "label")],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@@ -193,5 +226,5 @@ class TestSetAndGetStringHigher:
|
||||
[(data.phys_out, "label"), (data.virt_out, "label")],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from vban_cmd import kinds
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
from vban_cmd import kinds
|
||||
|
||||
|
||||
class TestPublicPacketLower:
|
||||
@@ -12,15 +10,11 @@ class TestPublicPacketLower:
|
||||
"""Tests for a valid rt data packet"""
|
||||
|
||||
def test_it_gets_an_rt_data_packet(self):
|
||||
assert tests.public_packet.voicemeetertype in (
|
||||
assert vban.public_packet.voicemeetertype in (
|
||||
kind.name for kind in kinds.kinds_all
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
@pytest.mark.parametrize("value", [0, 1])
|
||||
class TestSetRT:
|
||||
__test__ = True
|
||||
@@ -35,7 +29,6 @@ class TestSetRT:
|
||||
],
|
||||
)
|
||||
def test_it_sends_a_text_request(self, kls, index, param, value):
|
||||
tests._set_rt(f"{kls}[{index}]", param, value)
|
||||
time.sleep(0.02)
|
||||
target = getattr(tests, kls)[index]
|
||||
vban._set_rt(f"{kls}[{index}].{param}", value)
|
||||
target = getattr(vban, kls)[index]
|
||||
assert getattr(target, param) == bool(value)
|
||||
|
||||
164
vban_cmd/base.py
164
vban_cmd/base.py
@@ -1,164 +0,0 @@
|
||||
import socket
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Iterable, NoReturn, Optional, Union
|
||||
|
||||
from .misc import Event
|
||||
from .packet import TextRequestHeader
|
||||
from .subject import Subject
|
||||
from .util import Socket, comp, script
|
||||
from .worker import Subscriber, Updater
|
||||
|
||||
|
||||
class VbanCmd(metaclass=ABCMeta):
|
||||
"""Base class responsible for communicating over VBAN RT Service"""
|
||||
|
||||
DELAY = 0.001
|
||||
# fmt: off
|
||||
BPS_OPTS = [
|
||||
0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
|
||||
38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
|
||||
1000000, 1500000, 2000000, 3000000,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for attr, val in kwargs.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
self.text_header = TextRequestHeader(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
)
|
||||
self.socks = tuple(
|
||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
||||
)
|
||||
self.subject = Subject()
|
||||
self.cache = dict()
|
||||
self.event = Event(self.subs)
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
self.login()
|
||||
return self
|
||||
|
||||
def login(self):
|
||||
"""Starts the subscriber and updater threads"""
|
||||
self.running = True
|
||||
|
||||
self.subscriber = Subscriber(self)
|
||||
self.subscriber.start()
|
||||
|
||||
self.updater = Updater(self)
|
||||
self.updater.start()
|
||||
|
||||
def _set_rt(
|
||||
self,
|
||||
id_: str,
|
||||
param: Optional[str] = None,
|
||||
val: Optional[Union[int, float]] = None,
|
||||
):
|
||||
"""Sends a string request command over a network."""
|
||||
cmd = id_ if not param else f"{id_}.{param}={val}"
|
||||
self.socks[Socket.request].sendto(
|
||||
self.text_header.header + cmd.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
count = int.from_bytes(self.text_header.framecounter, "little") + 1
|
||||
self.text_header.framecounter = count.to_bytes(4, "little")
|
||||
if param:
|
||||
self.cache[f"{id_}.{param}"] = val
|
||||
if self.sync:
|
||||
time.sleep(0.02)
|
||||
|
||||
@script
|
||||
def sendtext(self, cmd):
|
||||
"""Sends a multiple parameter string over a network."""
|
||||
self._set_rt(cmd)
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation."""
|
||||
return self.public_packet.voicemeetertype
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
v1, v2, v3, v4 = self.public_packet.voicemeeterversion
|
||||
return f"{v1}.{v2}.{v3}.{v4}"
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
"""True iff a parameter has changed"""
|
||||
return self._pdirty
|
||||
|
||||
@property
|
||||
def ldirty(self):
|
||||
"""True iff a level value has changed."""
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(not x for x in comp(self.cache["strip_level"], self._strip_buf)),
|
||||
tuple(not x for x in comp(self.cache["bus_level"], self._bus_buf)),
|
||||
)
|
||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
||||
|
||||
@property
|
||||
def public_packet(self):
|
||||
return self._public_packet
|
||||
|
||||
def clear_dirty(self):
|
||||
while self.pdirty:
|
||||
pass
|
||||
|
||||
def _get_levels(self, packet) -> Iterable:
|
||||
"""
|
||||
returns both level arrays (strip_levels, bus_levels) BEFORE math conversion
|
||||
|
||||
strip levels in PREFADER mode.
|
||||
"""
|
||||
return (
|
||||
tuple(val for val in packet.inputlevels),
|
||||
tuple(val for val in packet.outputlevels),
|
||||
)
|
||||
|
||||
def apply(self, data: dict):
|
||||
"""
|
||||
Sets all parameters of a dict
|
||||
|
||||
minor delay between each recursion
|
||||
"""
|
||||
|
||||
def param(key):
|
||||
obj, m2, *rem = key.split("-")
|
||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
||||
if obj in ("strip", "bus"):
|
||||
return getattr(self, obj)[index]
|
||||
else:
|
||||
raise ValueError(obj)
|
||||
|
||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
||||
|
||||
def apply_config(self, name):
|
||||
"""applies a config from memory"""
|
||||
error_msg = (
|
||||
f"No config with name '{name}' is loaded into memory",
|
||||
f"Known configs: {list(self.configs.keys())}",
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
print(f"Profile '{name}' applied!")
|
||||
except KeyError as e:
|
||||
print(("\n").join(error_msg))
|
||||
|
||||
def logout(self):
|
||||
self.running = False
|
||||
time.sleep(0.2)
|
||||
[sock.close() for sock in self.socks]
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
self.logout()
|
||||
@@ -26,28 +26,48 @@ class Bus(IRemote):
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}]"
|
||||
return f"bus[{self.index}]"
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = self.public_packet.busgain[self.index]
|
||||
if val < 10000:
|
||||
return -val
|
||||
elif val == ((1 << 16) - 1):
|
||||
return 0
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter("gain")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class BusEQ(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
BUSEQ_cls = type(
|
||||
f"BusEQ{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
**{param: channel_bool_prop(param) for param in ["on", "ab"]},
|
||||
},
|
||||
)
|
||||
return BUSEQ_cls(remote, index)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"bus[{self.index}].eq"
|
||||
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
def __str__(self):
|
||||
@@ -79,14 +99,24 @@ class BusLevel(IRemote):
|
||||
def getter(self):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||
|
||||
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||
return tuple(
|
||||
round(-i * 0.01, 1)
|
||||
fget(i)
|
||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
||||
)
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packet)[1][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}]"
|
||||
return f"bus[{self.index}]"
|
||||
|
||||
@property
|
||||
def all(self) -> tuple:
|
||||
@@ -108,7 +138,7 @@ def _make_bus_mode_mixin():
|
||||
"""Creates a mixin of Bus Modes."""
|
||||
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}].mode"
|
||||
return f"bus[{self.index}].mode"
|
||||
|
||||
def get(self):
|
||||
time.sleep(0.01)
|
||||
@@ -154,11 +184,10 @@ def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
f"{BUS_cls.__name__}{remote.kind}",
|
||||
(BUS_cls,),
|
||||
{
|
||||
"eq": BusEQ.make(remote, i),
|
||||
"levels": BusLevel(remote, i),
|
||||
"mode": BUSMODEMIXIN_cls(remote, i),
|
||||
**{param: channel_bool_prop(param) for param in ["mute", "mono"]},
|
||||
"eq": channel_bool_prop("eq.On"),
|
||||
"eq_ab": channel_bool_prop("eq.ab"),
|
||||
"label": channel_label_prop(),
|
||||
},
|
||||
)(remote, i)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from .error import VMCMDErrors
|
||||
from .iremote import IRemote
|
||||
from .meta import action_prop
|
||||
from .meta import action_fn
|
||||
|
||||
|
||||
class Command(IRemote):
|
||||
@@ -22,17 +21,16 @@ class Command(IRemote):
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
param: action_prop(param)
|
||||
for param in ["show", "shutdown", "restart"]
|
||||
param: action_fn(param) for param in ["show", "shutdown", "restart"]
|
||||
},
|
||||
"hide": action_prop("show", val=0),
|
||||
"hide": action_fn("show", val=0),
|
||||
},
|
||||
)
|
||||
return CMD_cls(remote)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return "Command"
|
||||
return "command"
|
||||
|
||||
def set_showvbanchat(self, val: bool):
|
||||
self.setter("DialogShow.VBANCHAT", 1 if val else 0)
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
import itertools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .error import VBANCMDError
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
from .kinds import request_kind_map as kindmap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TOMLStrBuilder:
|
||||
"""builds a config profile, as a string, for the toml parser"""
|
||||
@@ -28,10 +36,18 @@ class TOMLStrBuilder:
|
||||
+ [f"B{i} = false" for i in range(1, self.kind.virt_out + 1)]
|
||||
)
|
||||
self.phys_strip_params = self.virt_strip_params + [
|
||||
"comp = 0.0",
|
||||
"gate = 0.0",
|
||||
"comp.knob = 0.0",
|
||||
"gate.knob = 0.0",
|
||||
"denoiser.knob = 0.0",
|
||||
"eq.on = false",
|
||||
]
|
||||
self.bus_float = ["gain = 0.0"]
|
||||
self.bus_params = [
|
||||
"mono = false",
|
||||
"eq.on = false",
|
||||
"mute = false",
|
||||
"gain = 0.0",
|
||||
]
|
||||
self.bus_bool = ["mono = false", "eq = false", "mute = false"]
|
||||
|
||||
if profile == "reset":
|
||||
self.reset_config()
|
||||
@@ -62,7 +78,7 @@ class TOMLStrBuilder:
|
||||
else self.virt_strip_params
|
||||
)
|
||||
case "bus":
|
||||
toml_str += ("\n").join(self.bus_bool)
|
||||
toml_str += ("\n").join(self.bus_params)
|
||||
case _:
|
||||
pass
|
||||
return toml_str + "\n"
|
||||
@@ -70,7 +86,6 @@ class TOMLStrBuilder:
|
||||
|
||||
class TOMLDataExtractor:
|
||||
def __init__(self, file):
|
||||
self._data = dict()
|
||||
with open(file, "rb") as f:
|
||||
self._data = tomllib.load(f)
|
||||
|
||||
@@ -118,6 +133,7 @@ class Loader(metaclass=SingletonType):
|
||||
|
||||
def __init__(self, kind):
|
||||
self._kind = kind
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._configs = dict()
|
||||
self.defaults(kind)
|
||||
self.parser = None
|
||||
@@ -129,14 +145,21 @@ class Loader(metaclass=SingletonType):
|
||||
|
||||
def parse(self, identifier, data):
|
||||
if identifier in self._configs:
|
||||
print(f"config file with name {identifier} already in memory, skipping..")
|
||||
return False
|
||||
self.logger.info(
|
||||
f"config file with name {identifier} already in memory, skipping.."
|
||||
)
|
||||
return
|
||||
try:
|
||||
self.parser = dataextraction_factory(data)
|
||||
except tomllib.TOMLDecodeError as e:
|
||||
ERR_MSG = (str(e), f"When attempting to load {identifier}.toml")
|
||||
self.logger.error(f"{type(e).__name__}: {' '.join(ERR_MSG)}")
|
||||
return
|
||||
return True
|
||||
|
||||
def register(self, identifier, data=None):
|
||||
self._configs[identifier] = data if data else self.parser.data
|
||||
print(f"config {self.name}/{identifier} loaded into memory")
|
||||
self.logger.info(f"config {self.name}/{identifier} loaded into memory")
|
||||
|
||||
def deregister(self):
|
||||
self._configs.clear()
|
||||
@@ -159,15 +182,16 @@ def loader(kind):
|
||||
|
||||
returns configs loaded into memory
|
||||
"""
|
||||
logger_loader = logger.getChild("loader")
|
||||
loader = Loader(kind)
|
||||
|
||||
for path in (
|
||||
Path.cwd() / "configs" / kind.name,
|
||||
Path(__file__).parent / "configs" / kind.name,
|
||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||
Path.home() / ".config" / "vban-cmd" / kind.name,
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs" / kind.name,
|
||||
):
|
||||
if path.is_dir():
|
||||
print(f"Checking [{path}] for TOML config files:")
|
||||
logger_loader.info(f"Checking [{path}] for TOML config files:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
if loader.parse(identifier, file):
|
||||
@@ -183,6 +207,6 @@ def request_config(kind_id: str):
|
||||
"""
|
||||
try:
|
||||
configs = loader(kindmap(kind_id))
|
||||
except KeyError as e:
|
||||
print(f"Unknown Voicemeeter kind '{kind_id}'")
|
||||
except KeyError:
|
||||
raise VBANCMDError(f"Unknown Voicemeeter kind {kind_id}")
|
||||
return configs
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
class VMCMDErrors(Exception):
|
||||
"""general errors"""
|
||||
class VBANCMDError(Exception):
|
||||
"""Base VBANCMD Exception class."""
|
||||
|
||||
pass
|
||||
|
||||
class VBANCMDConnectionError(VBANCMDError):
|
||||
"""Exception raised when connection/timeout errors occur"""
|
||||
|
||||
56
vban_cmd/event.py
Normal file
56
vban_cmd/event.py
Normal file
@@ -0,0 +1,56 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Event:
|
||||
"""Keeps track of event subscriptions"""
|
||||
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
def info(self, msg=None):
|
||||
info = (f"{msg} events",) if msg else ()
|
||||
if self.any():
|
||||
info += (f"now listening for {', '.join(self.get())} events",)
|
||||
else:
|
||||
info += (f"not listening for any events",)
|
||||
self.logger.info(", ".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@pdirty.setter
|
||||
def pdirty(self, val: bool):
|
||||
self.subs["pdirty"] = val
|
||||
self.info(f"pdirty {'added to' if val else 'removed from'}")
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
return self.subs["ldirty"]
|
||||
|
||||
@ldirty.setter
|
||||
def ldirty(self, val: bool):
|
||||
self.subs["ldirty"] = val
|
||||
self.info(f"ldirty {'added to' if val else 'removed from'}")
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, True)
|
||||
|
||||
def remove(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, False)
|
||||
@@ -1,15 +1,21 @@
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from functools import cached_property
|
||||
from typing import Iterable, NoReturn, Self
|
||||
from typing import Iterable
|
||||
|
||||
from .base import VbanCmd
|
||||
from .bus import request_bus_obj as bus
|
||||
from .command import Command
|
||||
from .config import request_config as configs
|
||||
from .error import VBANCMDError
|
||||
from .kinds import KindMapClass
|
||||
from .kinds import request_kind_map as kindmap
|
||||
from .macrobutton import MacroButton
|
||||
from .strip import request_strip_obj as strip
|
||||
from .vban import request_vban_obj as vban
|
||||
from .vbancmd import VbanCmd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FactoryBuilder:
|
||||
@@ -19,7 +25,9 @@ class FactoryBuilder:
|
||||
Separates construction from representation.
|
||||
"""
|
||||
|
||||
BuilderProgress = IntEnum("BuilderProgress", "strip bus command", start=0)
|
||||
BuilderProgress = IntEnum(
|
||||
"BuilderProgress", "strip bus command macrobutton vban", start=0
|
||||
)
|
||||
|
||||
def __init__(self, factory, kind: KindMapClass):
|
||||
self._factory = factory
|
||||
@@ -28,39 +36,47 @@ class FactoryBuilder:
|
||||
f"Finished building strips for {self._factory}",
|
||||
f"Finished building buses for {self._factory}",
|
||||
f"Finished building commands for {self._factory}",
|
||||
f"Finished building macrobuttons for {self._factory}",
|
||||
f"Finished building vban in/out streams for {self._factory}",
|
||||
)
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
def _pinfo(self, name: str) -> NoReturn:
|
||||
def _pinfo(self, name: str) -> None:
|
||||
"""prints progress status for each step"""
|
||||
name = name.split("_")[1]
|
||||
print(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
|
||||
def make_strip(self) -> Self:
|
||||
def make_strip(self):
|
||||
self._factory.strip = tuple(
|
||||
strip(i < self.kind.phys_in, self._factory, i)
|
||||
for i in range(self.kind.num_strip)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_bus(self) -> Self:
|
||||
def make_bus(self):
|
||||
self._factory.bus = tuple(
|
||||
bus(i < self.kind.phys_out, self._factory, i)
|
||||
for i in range(self.kind.num_bus)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_command(self) -> Self:
|
||||
def make_command(self):
|
||||
self._factory.command = Command.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_macrobutton(self):
|
||||
self._factory.button = tuple(MacroButton(self._factory, i) for i in range(80))
|
||||
return self
|
||||
|
||||
def make_vban(self):
|
||||
self._factory.vban = vban(self._factory)
|
||||
return self
|
||||
|
||||
|
||||
class FactoryBase(VbanCmd):
|
||||
"""Base class for factories, subclasses VbanCmd."""
|
||||
|
||||
def __init__(self, kind_id: str, **kwargs):
|
||||
defaultsubs = {"pdirty": True, "ldirty": False}
|
||||
if "subs" in kwargs:
|
||||
defaultsubs = defaultsubs | kwargs.pop("subs")
|
||||
defaultkwargs = {
|
||||
"ip": None,
|
||||
"port": 6980,
|
||||
@@ -68,9 +84,14 @@ class FactoryBase(VbanCmd):
|
||||
"bps": 0,
|
||||
"channel": 0,
|
||||
"ratelimit": 0.01,
|
||||
"timeout": 5,
|
||||
"outbound": False,
|
||||
"sync": False,
|
||||
"subs": defaultsubs,
|
||||
"pdirty": False,
|
||||
"ldirty": False,
|
||||
}
|
||||
if "subs" in kwargs:
|
||||
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
||||
kwargs = defaultkwargs | kwargs
|
||||
self.kind = kindmap(kind_id)
|
||||
super().__init__(**kwargs)
|
||||
@@ -79,12 +100,20 @@ class FactoryBase(VbanCmd):
|
||||
self.builder.make_strip,
|
||||
self.builder.make_bus,
|
||||
self.builder.make_command,
|
||||
self.builder.make_macrobutton,
|
||||
self.builder.make_vban,
|
||||
)
|
||||
self._configs = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Voicemeeter {self.kind}"
|
||||
|
||||
def __repr__(self):
|
||||
return (
|
||||
type(self).__name__
|
||||
+ f"({self.kind}, ip='{self.ip}', port={self.port}, streamname='{self.streamname}')"
|
||||
)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def steps(self):
|
||||
@@ -186,9 +215,12 @@ def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
||||
|
||||
Returns a reference to a VbanCmd class of a kind
|
||||
"""
|
||||
logger_entry = logger.getChild("factory.request_vbancmd_obj")
|
||||
|
||||
VBANCMD_obj = None
|
||||
try:
|
||||
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise SystemExit(e)
|
||||
logger_entry.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDError(str(e)) from e
|
||||
return VBANCMD_obj
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Modes:
|
||||
@@ -26,9 +29,9 @@ class Modes:
|
||||
|
||||
_mask: hex = 0x000000F0
|
||||
|
||||
_eq_on: hex = 0x00000100
|
||||
_on: hex = 0x00000100 # eq.on
|
||||
_cross: hex = 0x00000200
|
||||
_eq_ab: hex = 0x00000800
|
||||
_ab: hex = 0x00000800 # eq.ab
|
||||
|
||||
_busa: hex = 0x00001000
|
||||
_busa1: hex = 0x00001000
|
||||
@@ -85,17 +88,29 @@ class IRemote(metaclass=ABCMeta):
|
||||
def __init__(self, remote, index=None):
|
||||
self._remote = remote
|
||||
self.index = index
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._modes = Modes()
|
||||
|
||||
def getter(self, param):
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f"getter: {cmd}")
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd)
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
|
||||
def setter(self, param, val):
|
||||
"""Sends a string request RT packet."""
|
||||
self._remote._set_rt(f"{self.identifier}", param, val)
|
||||
self.logger.debug(f"setter: {self._cmd(param)}={val}")
|
||||
self._remote._set_rt(self._cmd(param), val)
|
||||
|
||||
def _cmd(self, param):
|
||||
cmd = (self.identifier,)
|
||||
if param:
|
||||
cmd += (f".{param}",)
|
||||
return "".join(cmd)
|
||||
|
||||
@property
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
pass
|
||||
@@ -111,20 +126,26 @@ class IRemote(metaclass=ABCMeta):
|
||||
def fget(attr, val):
|
||||
if attr == "mode":
|
||||
return (f"mode.{val}", 1)
|
||||
elif attr == "knob":
|
||||
return ("", val)
|
||||
return (attr, val)
|
||||
|
||||
script = str()
|
||||
for attr, val in data.items():
|
||||
if hasattr(self, attr):
|
||||
if not isinstance(val, dict):
|
||||
if attr in dir(self): # avoid calling getattr (with hasattr)
|
||||
attr, val = fget(attr, val)
|
||||
if isinstance(val, bool):
|
||||
val = 1 if val else 0
|
||||
|
||||
self._remote.cache[f"{self.identifier}[{self.index}].{attr}"] = val
|
||||
script += f"{self.identifier}[{self.index}].{attr}={val};"
|
||||
self._remote.cache[self._cmd(attr)] = val
|
||||
self._remote._script += f"{self._cmd(attr)}={val};"
|
||||
else:
|
||||
target = getattr(self, attr)
|
||||
target.apply(val)
|
||||
|
||||
self._remote.sendtext(script)
|
||||
self._remote.sendtext(self._remote._script)
|
||||
return self
|
||||
|
||||
def then_wait(self):
|
||||
self._remote._script = str()
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
from .error import VBANCMDError
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
@@ -51,6 +53,14 @@ class KindMapClass(metaclass=SingletonType):
|
||||
def num_bus(self):
|
||||
return sum(self.outs)
|
||||
|
||||
@property
|
||||
def num_strip_levels(self) -> int:
|
||||
return 2 * self.phys_in + 8 * self.virt_in
|
||||
|
||||
@property
|
||||
def num_bus_levels(self) -> int:
|
||||
return 8 * (self.phys_out + self.virt_out)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.name.capitalize()
|
||||
|
||||
@@ -60,7 +70,7 @@ class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4)
|
||||
vban: tuple = (4, 4, 1, 1)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -68,7 +78,7 @@ class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -76,7 +86,7 @@ class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
@@ -97,7 +107,7 @@ def request_kind_map(kind_id):
|
||||
try:
|
||||
KIND_obj = kind_factory(kind_id)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
raise VBANCMDError(str(e)) from e
|
||||
return KIND_obj
|
||||
|
||||
|
||||
|
||||
36
vban_cmd/macrobutton.py
Normal file
36
vban_cmd/macrobutton.py
Normal file
@@ -0,0 +1,36 @@
|
||||
from .iremote import IRemote
|
||||
|
||||
|
||||
class MacroButton(IRemote):
|
||||
"""A placeholder class in case this interface is being used interchangeably with the Remote API"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
return f"command.button[{self.index}]"
|
||||
|
||||
@property
|
||||
def state(self) -> bool:
|
||||
self.logger.warning("button.state commands are not supported over VBAN")
|
||||
|
||||
@state.setter
|
||||
def state(self, _):
|
||||
self.logger.warning("button.state commands are not supported over VBAN")
|
||||
|
||||
@property
|
||||
def stateonly(self) -> bool:
|
||||
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
||||
|
||||
@stateonly.setter
|
||||
def stateonly(self, v):
|
||||
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
||||
|
||||
@property
|
||||
def trigger(self) -> bool:
|
||||
self.logger.warning("button.trigger commands are not supported over VBAN")
|
||||
|
||||
@trigger.setter
|
||||
def trigger(self, _):
|
||||
self.logger.warning("button.trigger commands are not supported over VBAN")
|
||||
@@ -1,6 +1,5 @@
|
||||
from functools import partial
|
||||
|
||||
from .error import VMCMDErrors
|
||||
from .util import cache_bool, cache_string
|
||||
|
||||
|
||||
@@ -17,7 +16,7 @@ def channel_bool_prop(param):
|
||||
)[self.index],
|
||||
"little",
|
||||
)
|
||||
& getattr(self._modes, f'_{param.replace(".", "_").lower()}')
|
||||
& getattr(self._modes, f"_{param.lower()}")
|
||||
== 0
|
||||
)
|
||||
|
||||
@@ -92,8 +91,8 @@ def bus_mode_prop(param):
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def action_prop(param, val=1):
|
||||
"""A param that performs an action"""
|
||||
def action_fn(param, val=1):
|
||||
"""A function that performs an action"""
|
||||
|
||||
def fdo(self):
|
||||
self.setter(param, val)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
class Event:
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
|
||||
def info(self, msg):
|
||||
info = (
|
||||
f"{msg} events",
|
||||
f"Now listening for {', '.join(self.get())} events",
|
||||
)
|
||||
print("\n".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@property
|
||||
def ldirty(self):
|
||||
return self.subs["ldirty"]
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, event):
|
||||
self.subs[event] = True
|
||||
self.info(f"{event} added to")
|
||||
|
||||
def remove(self, event):
|
||||
self.subs[event] = False
|
||||
self.info(f"{event} removed from")
|
||||
@@ -1,47 +1,66 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
from .kinds import KindMapClass
|
||||
from .util import comp
|
||||
|
||||
VBAN_PROTOCOL_TXT = 0x40
|
||||
VBAN_PROTOCOL_SERVICE = 0x60
|
||||
|
||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||
VBAN_SERVICE_RTPACKET = 33
|
||||
|
||||
MAX_PACKET_SIZE = 1436
|
||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||
|
||||
|
||||
@dataclass
|
||||
class VBAN_VMRT_Packet_Data:
|
||||
"""Represents the structure of a VMRT data packet"""
|
||||
class VbanRtPacket:
|
||||
"""Represents the body of a VBAN RT data packet"""
|
||||
|
||||
_voicemeeterType: bytes
|
||||
_reserved: bytes
|
||||
_buffersize: bytes
|
||||
_voicemeeterVersion: bytes
|
||||
_optionBits: bytes
|
||||
_samplerate: bytes
|
||||
_inputLeveldB100: bytes
|
||||
_outputLeveldB100: bytes
|
||||
_TransportBit: bytes
|
||||
_stripState: bytes
|
||||
_busState: bytes
|
||||
_stripGaindB100Layer1: bytes
|
||||
_stripGaindB100Layer2: bytes
|
||||
_stripGaindB100Layer3: bytes
|
||||
_stripGaindB100Layer4: bytes
|
||||
_stripGaindB100Layer5: bytes
|
||||
_stripGaindB100Layer6: bytes
|
||||
_stripGaindB100Layer7: bytes
|
||||
_stripGaindB100Layer8: bytes
|
||||
_busGaindB100: bytes
|
||||
_stripLabelUTF8c60: bytes
|
||||
_busLabelUTF8c60: bytes
|
||||
_kind: KindMapClass
|
||||
_voicemeeterType: bytes # data[28:29]
|
||||
_reserved: bytes # data[29:30]
|
||||
_buffersize: bytes # data[30:32]
|
||||
_voicemeeterVersion: bytes # data[32:36]
|
||||
_optionBits: bytes # data[36:40]
|
||||
_samplerate: bytes # data[40:44]
|
||||
_inputLeveldB100: bytes # data[44:112]
|
||||
_outputLeveldB100: bytes # data[112:240]
|
||||
_TransportBit: bytes # data[240:244]
|
||||
_stripState: bytes # data[244:276]
|
||||
_busState: bytes # data[276:308]
|
||||
_stripGaindB100Layer1: bytes # data[308:324]
|
||||
_stripGaindB100Layer2: bytes # data[324:340]
|
||||
_stripGaindB100Layer3: bytes # data[340:356]
|
||||
_stripGaindB100Layer4: bytes # data[356:372]
|
||||
_stripGaindB100Layer5: bytes # data[372:388]
|
||||
_stripGaindB100Layer6: bytes # data[388:404]
|
||||
_stripGaindB100Layer7: bytes # data[404:420]
|
||||
_stripGaindB100Layer8: bytes # data[420:436]
|
||||
_busGaindB100: bytes # data[436:452]
|
||||
_stripLabelUTF8c60: bytes # data[452:932]
|
||||
_busLabelUTF8c60: bytes # data[932:1412]
|
||||
|
||||
def pdirty(self, other):
|
||||
def _generate_levels(self, levelarray) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(levelarray[i : i + 2], "little")
|
||||
for i in range(0, len(levelarray), 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def strip_levels(self):
|
||||
return self._generate_levels(self._inputLeveldB100)
|
||||
|
||||
@property
|
||||
def bus_levels(self):
|
||||
return self._generate_levels(self._outputLeveldB100)
|
||||
|
||||
def pdirty(self, other) -> bool:
|
||||
"""True iff any defined parameter has changed"""
|
||||
|
||||
return not (
|
||||
self._stripState == other._stripState
|
||||
and self._busState == other._busState
|
||||
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
|
||||
and self._busLabelUTF8c60 == other._busLabelUTF8c60
|
||||
and self._stripGaindB100Layer1 == other._stripGaindB100Layer1
|
||||
and self._stripGaindB100Layer2 == other._stripGaindB100Layer2
|
||||
and self._stripGaindB100Layer3 == other._stripGaindB100Layer3
|
||||
@@ -51,8 +70,17 @@ class VBAN_VMRT_Packet_Data:
|
||||
and self._stripGaindB100Layer7 == other._stripGaindB100Layer7
|
||||
and self._stripGaindB100Layer8 == other._stripGaindB100Layer8
|
||||
and self._busGaindB100 == other._busGaindB100
|
||||
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
|
||||
and self._busLabelUTF8c60 == other._busLabelUTF8c60
|
||||
)
|
||||
|
||||
def ldirty(self, strip_cache, bus_cache) -> bool:
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(not val for val in comp(strip_cache, self.strip_levels)),
|
||||
tuple(not val for val in comp(bus_cache, self.bus_levels)),
|
||||
)
|
||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
||||
|
||||
@property
|
||||
def voicemeetertype(self) -> str:
|
||||
"""returns voicemeeter type as a string"""
|
||||
@@ -77,24 +105,14 @@ class VBAN_VMRT_Packet_Data:
|
||||
return int.from_bytes(self._samplerate, "little")
|
||||
|
||||
@property
|
||||
def inputlevels(self) -> Generator[float, None, None]:
|
||||
"""returns the entire level array across all inputs"""
|
||||
for i in range(0, 68, 2):
|
||||
val = ((1 << 16) - 1) - int.from_bytes(
|
||||
self._inputLeveldB100[i : i + 2], "little"
|
||||
)
|
||||
if val != ((1 << 16) - 1):
|
||||
yield val
|
||||
def inputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all inputs for a kind"""
|
||||
return self.strip_levels[0 : self._kind.num_strip_levels]
|
||||
|
||||
@property
|
||||
def outputlevels(self) -> Generator[float, None, None]:
|
||||
"""returns the entire level array across all outputs"""
|
||||
for i in range(0, 128, 2):
|
||||
val = ((1 << 16) - 1) - int.from_bytes(
|
||||
self._outputLeveldB100[i : i + 2], "little"
|
||||
)
|
||||
if val != ((1 << 16) - 1):
|
||||
yield val
|
||||
def outputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all outputs for a kind"""
|
||||
return self.bus_levels[0 : self._kind.num_bus_levels]
|
||||
|
||||
@property
|
||||
def stripstate(self) -> tuple:
|
||||
@@ -114,64 +132,56 @@ class VBAN_VMRT_Packet_Data:
|
||||
@property
|
||||
def stripgainlayer1(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer2(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer2[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer2[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer3(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer3[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer3[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer4(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer4[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer4[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer5(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer5[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer5[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer6(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer6[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer6[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer7(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer7[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer7[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer8(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer8[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer8[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@@ -179,7 +189,7 @@ class VBAN_VMRT_Packet_Data:
|
||||
def busgain(self) -> tuple:
|
||||
"""returns tuple of bus gains"""
|
||||
return tuple(
|
||||
((1 << 16) - 1) - int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
||||
int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@@ -201,12 +211,41 @@ class VBAN_VMRT_Packet_Data:
|
||||
|
||||
|
||||
@dataclass
|
||||
class VBAN_VMRT_Packet_Header:
|
||||
"""Represents a RESPONSE RT PACKET header"""
|
||||
class SubscribeHeader:
|
||||
"""Represents the header an RT Packet Service subscription packet"""
|
||||
|
||||
name = "Register RTP"
|
||||
timeout = 15
|
||||
vban: bytes = "VBAN".encode()
|
||||
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
||||
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
||||
framecounter: bytes = (0).to_bytes(4, "little")
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.format_sr
|
||||
header += self.format_nbs
|
||||
header += self.format_nbc
|
||||
header += self.format_bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert (
|
||||
len(header) == HEADER_SIZE + 4
|
||||
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||
return header
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketHeader:
|
||||
"""Represents the header of a VBAN RT response packet"""
|
||||
|
||||
name = "Voicemeeter-RTP"
|
||||
vban: bytes = "VBAN".encode()
|
||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
||||
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
||||
format_bit: bytes = (0).to_bytes(1, "little")
|
||||
@@ -220,13 +259,13 @@ class VBAN_VMRT_Packet_Header:
|
||||
header += self.format_nbc
|
||||
header += self.format_bit
|
||||
header += self.streamname
|
||||
assert len(header) == HEADER_SIZE - 4, f"Header expected {HEADER_SIZE-4} bytes"
|
||||
assert len(header) == HEADER_SIZE, f"expected header size {HEADER_SIZE} bytes"
|
||||
return header
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextRequestHeader:
|
||||
"""Represents a REQUEST RT PACKET header"""
|
||||
class RequestHeader:
|
||||
"""Represents the header of a REQUEST RT PACKET"""
|
||||
|
||||
name: str
|
||||
bps_index: int
|
||||
@@ -238,7 +277,7 @@ class TextRequestHeader:
|
||||
|
||||
@property
|
||||
def sr(self):
|
||||
return (0x40 + self.bps_index).to_bytes(1, "little")
|
||||
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, "little")
|
||||
|
||||
@property
|
||||
def nbc(self):
|
||||
@@ -257,32 +296,7 @@ class TextRequestHeader:
|
||||
header += self.bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
||||
return header
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegisterRTHeader:
|
||||
"""Represents a REGISTER RT PACKET header"""
|
||||
|
||||
name = "Register RTP"
|
||||
timeout = 15
|
||||
vban: bytes = "VBAN".encode()
|
||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
||||
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
||||
framecounter: bytes = (0).to_bytes(4, "little")
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.format_sr
|
||||
header += self.format_nbs
|
||||
header += self.format_nbc
|
||||
header += self.format_bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
||||
assert (
|
||||
len(header) == HEADER_SIZE + 4
|
||||
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||
return header
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from typing import Union
|
||||
|
||||
@@ -19,7 +20,7 @@ class Strip(IRemote):
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
return f"strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
@@ -40,27 +41,32 @@ class Strip(IRemote):
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
return type(
|
||||
f"PhysicalStrip{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
"comp": StripComp(remote, index),
|
||||
"gate": StripGate(remote, index),
|
||||
"denoiser": StripDenoiser(remote, index),
|
||||
"eq": StripEQ(remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@property
|
||||
def comp(self) -> float:
|
||||
return
|
||||
|
||||
@comp.setter
|
||||
def comp(self, val: float):
|
||||
self.setter("Comp", val)
|
||||
|
||||
@property
|
||||
def gate(self) -> float:
|
||||
return
|
||||
|
||||
@gate.setter
|
||||
def gate(self, val: float):
|
||||
self.setter("gate", val)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return
|
||||
@@ -70,6 +76,182 @@ class PhysicalStrip(Strip):
|
||||
return
|
||||
|
||||
|
||||
class StripComp(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"strip[{self.index}].comp"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
@property
|
||||
def gainin(self) -> float:
|
||||
return
|
||||
|
||||
@gainin.setter
|
||||
def gainin(self, val: float):
|
||||
self.setter("GainIn", val)
|
||||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
return
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, val: float):
|
||||
self.setter("Ratio", val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter("Threshold", val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter("Attack", val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
self.setter("Release", val)
|
||||
|
||||
@property
|
||||
def knee(self) -> float:
|
||||
return
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: float):
|
||||
self.setter("Knee", val)
|
||||
|
||||
@property
|
||||
def gainout(self) -> float:
|
||||
return
|
||||
|
||||
@gainout.setter
|
||||
def gainout(self, val: float):
|
||||
self.setter("GainOut", val)
|
||||
|
||||
@property
|
||||
def makeup(self) -> bool:
|
||||
return
|
||||
|
||||
@makeup.setter
|
||||
def makeup(self, val: bool):
|
||||
self.setter("makeup", 1 if val else 0)
|
||||
|
||||
|
||||
class StripGate(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"strip[{self.index}].gate"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter("Threshold", val)
|
||||
|
||||
@property
|
||||
def damping(self) -> float:
|
||||
return
|
||||
|
||||
@damping.setter
|
||||
def damping(self, val: float):
|
||||
self.setter("Damping", val)
|
||||
|
||||
@property
|
||||
def bpsidechain(self) -> int:
|
||||
return
|
||||
|
||||
@bpsidechain.setter
|
||||
def bpsidechain(self, val: int):
|
||||
self.setter("BPSidechain", val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter("Attack", val)
|
||||
|
||||
@property
|
||||
def hold(self) -> float:
|
||||
return
|
||||
|
||||
@hold.setter
|
||||
def hold(self, val: float):
|
||||
self.setter("Hold", val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
self.setter("Release", val)
|
||||
|
||||
|
||||
class StripDenoiser(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"strip[{self.index}].denoiser"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
|
||||
class StripEQ(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"strip[{self.index}].eq"
|
||||
|
||||
@property
|
||||
def on(self):
|
||||
return
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def ab(self):
|
||||
return
|
||||
|
||||
@ab.setter
|
||||
def ab(self, val: bool):
|
||||
self.setter("ab", 1 if val else 0)
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
@@ -86,6 +268,12 @@ class VirtualStrip(Strip):
|
||||
def k(self, val: int):
|
||||
self.setter("karaoke", val)
|
||||
|
||||
def appgain(self, name: str, gain: float):
|
||||
self.setter("AppGain", f'("{name}", {gain})')
|
||||
|
||||
def appmute(self, name: str, mute: bool = None):
|
||||
self.setter("AppMute", f'("{name}", {1 if mute else 0})')
|
||||
|
||||
|
||||
class StripLevel(IRemote):
|
||||
def __init__(self, remote, index):
|
||||
@@ -103,14 +291,28 @@ class StripLevel(IRemote):
|
||||
self.range = self.level_map[self.index]
|
||||
|
||||
def getter(self):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||
|
||||
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||
return tuple(
|
||||
round(-i * 0.01, 1)
|
||||
for i in self._remote.cache["strip_level"][self.range[0] : self.range[-1]]
|
||||
fget(i)
|
||||
for i in self._remote.cache["strip_level"][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packet)[0][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
return f"strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def prefader(self) -> tuple:
|
||||
@@ -143,23 +345,18 @@ class GainLayer(IRemote):
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
return f"strip[{self.index}]"
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = getattr(self.public_packet, f"stripgainlayer{self._i+1}")[self.index]
|
||||
if val < 10000:
|
||||
return -val
|
||||
elif val == ((1 << 16) - 1):
|
||||
return 0
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter(f"GainLayer[{self._i}]")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
@@ -208,7 +405,7 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
||||
|
||||
Returns a physical or virtual strip subclass
|
||||
"""
|
||||
STRIP_cls = PhysicalStrip if is_phys_strip else VirtualStrip
|
||||
STRIP_cls = PhysicalStrip.make(remote, i) if is_phys_strip else VirtualStrip
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
class Subject:
|
||||
"""Adds support for observers"""
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subject:
|
||||
def __init__(self):
|
||||
"""list of current observers"""
|
||||
"""Adds support for observers and callbacks"""
|
||||
|
||||
self._observers = list()
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def observers(self) -> list:
|
||||
@@ -12,28 +16,57 @@ class Subject:
|
||||
|
||||
return self._observers
|
||||
|
||||
def notify(self, modifier=None):
|
||||
def notify(self, event):
|
||||
"""run callbacks on update"""
|
||||
|
||||
[o.on_update(modifier) for o in self._observers]
|
||||
for o in self._observers:
|
||||
if hasattr(o, "on_update"):
|
||||
o.on_update(event)
|
||||
else:
|
||||
if o.__name__ == f"on_{event}":
|
||||
o()
|
||||
|
||||
def add(self, observer):
|
||||
"""adds an observer to _observers"""
|
||||
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
else:
|
||||
print(f"Failed to add: {observer}")
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from _observers"""
|
||||
"""adds an observer to observers"""
|
||||
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
iterator = iter(observer)
|
||||
for o in iterator:
|
||||
if o not in self._observers:
|
||||
self._observers.append(o)
|
||||
self.logger.info(f"{o} added to event observers")
|
||||
else:
|
||||
self.logger.error(f"Failed to add {o} to event observers")
|
||||
except TypeError:
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
self.logger.info(f"{observer} added to event observers")
|
||||
else:
|
||||
self.logger.error(f"Failed to add {observer} to event observers")
|
||||
|
||||
register = add
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from observers"""
|
||||
|
||||
try:
|
||||
iterator = iter(observer)
|
||||
for o in iterator:
|
||||
try:
|
||||
self._observers.remove(o)
|
||||
self.logger.info(f"{o} removed from event observers")
|
||||
except ValueError:
|
||||
print(f"Failed to remove: {observer}")
|
||||
self.logger.error(f"Failed to remove {o} from event observers")
|
||||
except TypeError:
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
self.logger.info(f"{observer} removed from event observers")
|
||||
except ValueError:
|
||||
self.logger.error(f"Failed to remove {observer} from event observers")
|
||||
|
||||
deregister = remove
|
||||
|
||||
def clear(self):
|
||||
"""clears the _observers list"""
|
||||
"""clears the observers list"""
|
||||
|
||||
self._observers.clear()
|
||||
|
||||
@@ -7,9 +7,10 @@ def cache_bool(func, param):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
self, *rem = args
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd) == 1
|
||||
if self._cmd(param) in self._remote.cache:
|
||||
return self._remote.cache.pop(self._cmd(param)) == 1
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -20,9 +21,10 @@ def cache_string(func, param):
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
self, *rem = args
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd)
|
||||
if self._cmd(param) in self._remote.cache:
|
||||
return self._remote.cache.pop(self._cmd(param))
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -61,10 +63,26 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
||||
|
||||
Evaluates equality of each member in both tuples.
|
||||
"""
|
||||
|
||||
for a, b in zip(t0, t1):
|
||||
if b <= 9500:
|
||||
if ((1 << 16) - 1) - b <= 7200:
|
||||
yield a == b
|
||||
else:
|
||||
yield True
|
||||
|
||||
|
||||
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]
|
||||
|
||||
|
||||
Socket = IntEnum("Socket", "register request response", start=0)
|
||||
|
||||
234
vban_cmd/vban.py
Normal file
234
vban_cmd/vban.py
Normal file
@@ -0,0 +1,234 @@
|
||||
from abc import abstractmethod
|
||||
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for vban stream
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"vban.{self.direction}stream[{self.index}]"
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return
|
||||
|
||||
@name.setter
|
||||
def name(self, val: str):
|
||||
self.setter("name", val)
|
||||
|
||||
@property
|
||||
def ip(self) -> str:
|
||||
return
|
||||
|
||||
@ip.setter
|
||||
def ip(self, val: str):
|
||||
self.setter("ip", val)
|
||||
|
||||
@property
|
||||
def port(self) -> int:
|
||||
return
|
||||
|
||||
@port.setter
|
||||
def port(self, val: int):
|
||||
if not 1024 <= val <= 65535:
|
||||
self.logger.warning(
|
||||
f"port got: {val} but expected a value from 1024 to 65535"
|
||||
)
|
||||
self.setter("port", val)
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return
|
||||
|
||||
@sr.setter
|
||||
def sr(self, val: int):
|
||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||
if val not in opts:
|
||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||
self.setter("sr", val)
|
||||
|
||||
@property
|
||||
def channel(self) -> int:
|
||||
return
|
||||
|
||||
@channel.setter
|
||||
def channel(self, val: int):
|
||||
if not 1 <= val <= 8:
|
||||
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
|
||||
self.setter("channel", val)
|
||||
|
||||
@property
|
||||
def bit(self) -> int:
|
||||
return
|
||||
|
||||
@bit.setter
|
||||
def bit(self, val: int):
|
||||
if val not in (16, 24):
|
||||
self.logger.warning(f"bit got: {val} but expected value 16 or 24")
|
||||
self.setter("bit", 1 if (val == 16) else 2)
|
||||
|
||||
@property
|
||||
def quality(self) -> int:
|
||||
return
|
||||
|
||||
@quality.setter
|
||||
def quality(self, val: int):
|
||||
if not 0 <= val <= 4:
|
||||
self.logger.warning(f"quality got: {val} but expected a value from 0 to 4")
|
||||
self.setter("quality", val)
|
||||
|
||||
@property
|
||||
def route(self) -> int:
|
||||
return
|
||||
|
||||
@route.setter
|
||||
def route(self, val: int):
|
||||
if not 0 <= val <= 8:
|
||||
self.logger.warning(f"route got: {val} but expected a value from 0 to 8")
|
||||
self.setter("route", val)
|
||||
|
||||
|
||||
class VbanInstream(VbanStream):
|
||||
"""
|
||||
class representing a vban instream
|
||||
|
||||
subclasses VbanStream
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return "in"
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return
|
||||
|
||||
@property
|
||||
def channel(self) -> int:
|
||||
return
|
||||
|
||||
@property
|
||||
def bit(self) -> int:
|
||||
return
|
||||
|
||||
|
||||
class VbanAudioInstream(VbanInstream):
|
||||
"""Represents a VBAN Audio Instream"""
|
||||
|
||||
|
||||
class VbanMidiInstream(VbanInstream):
|
||||
"""Represents a VBAN Midi Instream"""
|
||||
|
||||
|
||||
class VbanTextInstream(VbanInstream):
|
||||
"""Represents a VBAN Text Instream"""
|
||||
|
||||
|
||||
class VbanOutstream(VbanStream):
|
||||
"""
|
||||
class representing a vban outstream
|
||||
|
||||
Subclasses VbanStream
|
||||
"""
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||
|
||||
@property
|
||||
def direction(self) -> str:
|
||||
return "out"
|
||||
|
||||
|
||||
class VbanAudioOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Audio Outstream"""
|
||||
|
||||
|
||||
class VbanMidiOutstream(VbanOutstream):
|
||||
"""Represents a VBAN Midi Outstream"""
|
||||
|
||||
|
||||
def _make_stream_pair(remote, kind):
|
||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||
|
||||
def _make_cls(i, direction):
|
||||
match direction:
|
||||
case "in":
|
||||
if i < num_instream:
|
||||
return VbanAudioInstream(remote, i)
|
||||
elif i < num_instream + num_midi:
|
||||
return VbanMidiInstream(remote, i)
|
||||
else:
|
||||
return VbanTextInstream(remote, i)
|
||||
case "out":
|
||||
if i < num_outstream:
|
||||
return VbanAudioOutstream(remote, i)
|
||||
else:
|
||||
return VbanMidiOutstream(remote, i)
|
||||
|
||||
return (
|
||||
tuple(_make_cls(i, "in") for i in range(num_instream + num_midi + num_text)),
|
||||
tuple(_make_cls(i, "out") for i in range(num_outstream + num_midi)),
|
||||
)
|
||||
|
||||
|
||||
def _make_stream_pairs(remote):
|
||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||
|
||||
|
||||
class Vban:
|
||||
"""
|
||||
class representing the vban module
|
||||
|
||||
Contains two tuples, one for each stream type
|
||||
"""
|
||||
|
||||
def __init__(self, remote):
|
||||
self.remote = remote
|
||||
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||
|
||||
def enable(self):
|
||||
"""if VBAN disabled there can be no communication with it"""
|
||||
|
||||
def disable(self):
|
||||
self.remote._set_rt("vban.Enable", 0)
|
||||
|
||||
|
||||
def vban_factory(remote) -> Vban:
|
||||
"""
|
||||
Factory method for vban
|
||||
|
||||
Returns a class that represents the VBAN module.
|
||||
"""
|
||||
VBAN_cls = Vban
|
||||
return type(f"{VBAN_cls.__name__}", (VBAN_cls,), {})(remote)
|
||||
|
||||
|
||||
def request_vban_obj(remote) -> Vban:
|
||||
"""
|
||||
Vban entry point.
|
||||
|
||||
Returns a reference to a Vban class of a kind
|
||||
"""
|
||||
return vban_factory(remote)
|
||||
240
vban_cmd/vbancmd.py
Normal file
240
vban_cmd/vbancmd.py
Normal file
@@ -0,0 +1,240 @@
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Iterable, Union
|
||||
|
||||
from .error import VBANCMDError
|
||||
from .event import Event
|
||||
from .packet import RequestHeader
|
||||
from .subject import Subject
|
||||
from .util import Socket, deep_merge, script
|
||||
from .worker import Producer, Subscriber, Updater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VbanCmd(metaclass=ABCMeta):
|
||||
"""Base class responsible for communicating with the VBAN RT Packet Service"""
|
||||
|
||||
DELAY = 0.001
|
||||
# fmt: off
|
||||
BPS_OPTS = [
|
||||
0, 110, 150, 300, 600, 1200, 2400, 4800, 9600, 14400, 19200, 31250,
|
||||
38400, 57600, 115200, 128000, 230400, 250000, 256000, 460800, 921600,
|
||||
1000000, 1500000, 2000000, 3000000,
|
||||
]
|
||||
# fmt: on
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.event = Event({k: kwargs.pop(k) for k in ("pdirty", "ldirty")})
|
||||
if not kwargs["ip"]:
|
||||
kwargs |= self._conn_from_toml()
|
||||
for attr, val in kwargs.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
self.packet_request = RequestHeader(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
)
|
||||
self.socks = tuple(
|
||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
||||
)
|
||||
self.subject = self.observer = Subject()
|
||||
self.cache = {}
|
||||
self._pdirty = False
|
||||
self._ldirty = False
|
||||
self._script = str()
|
||||
self.stop_event = None
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
|
||||
def _conn_from_toml(self) -> dict:
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
def get_filepath():
|
||||
filepaths = [
|
||||
Path.cwd() / "vban.toml",
|
||||
Path.cwd() / "configs" / "vban.toml",
|
||||
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs" / "vban.toml",
|
||||
]
|
||||
for filepath in filepaths:
|
||||
if filepath.exists():
|
||||
return filepath
|
||||
|
||||
if filepath := get_filepath():
|
||||
with open(filepath, "rb") as f:
|
||||
conn = tomllib.load(f)
|
||||
assert (
|
||||
"connection" in conn and "ip" in conn["connection"]
|
||||
), "expected [connection][ip] in vban config"
|
||||
return conn["connection"]
|
||||
raise VBANCMDError("no ip provided and no vban.toml located.")
|
||||
|
||||
def __enter__(self):
|
||||
self.login()
|
||||
return self
|
||||
|
||||
def login(self) -> None:
|
||||
"""Starts the subscriber and updater threads (unless in outbound mode)"""
|
||||
if not self.outbound:
|
||||
self.event.info()
|
||||
|
||||
self.stop_event = threading.Event()
|
||||
self.stop_event.clear()
|
||||
self.subscriber = Subscriber(self, self.stop_event)
|
||||
self.subscriber.start()
|
||||
|
||||
queue = Queue()
|
||||
self.updater = Updater(self, queue)
|
||||
self.updater.start()
|
||||
self.producer = Producer(self, queue, self.stop_event)
|
||||
self.producer.start()
|
||||
|
||||
self.logger.info(
|
||||
"Successfully logged into VBANCMD {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format(
|
||||
**self.__dict__
|
||||
)
|
||||
)
|
||||
|
||||
def stopped(self):
|
||||
return self.stop_event is None or self.stop_event.is_set()
|
||||
|
||||
def _set_rt(self, cmd: str, val: Union[str, float]):
|
||||
"""Sends a string request command over a network."""
|
||||
self.socks[Socket.request].sendto(
|
||||
self.packet_request.header + f"{cmd}={val};".encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
self.cache[cmd] = val
|
||||
|
||||
@script
|
||||
def sendtext(self, script):
|
||||
"""Sends a multiple parameter string over a network."""
|
||||
self.socks[Socket.request].sendto(
|
||||
self.packet_request.header + script.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
self.logger.debug(f"sendtext: {script}")
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation."""
|
||||
return self.public_packet.voicemeetertype
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
return "{0}.{1}.{2}.{3}".format(*self.public_packet.voicemeeterversion)
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
"""True iff a parameter has changed"""
|
||||
return self._pdirty
|
||||
|
||||
@property
|
||||
def ldirty(self):
|
||||
"""True iff a level value has changed."""
|
||||
return self._ldirty
|
||||
|
||||
@property
|
||||
def public_packet(self):
|
||||
return self._public_packet
|
||||
|
||||
def clear_dirty(self) -> None:
|
||||
while self.pdirty:
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
def _get_levels(self, packet) -> Iterable:
|
||||
"""
|
||||
returns both level arrays (strip_levels, bus_levels) BEFORE math conversion
|
||||
|
||||
strip levels in PREFADER mode.
|
||||
"""
|
||||
return (
|
||||
packet.inputlevels,
|
||||
packet.outputlevels,
|
||||
)
|
||||
|
||||
def apply(self, data: dict):
|
||||
"""
|
||||
Sets all parameters of a dict
|
||||
|
||||
minor delay between each recursion
|
||||
"""
|
||||
|
||||
def target(key):
|
||||
match key.split("-"):
|
||||
case ["strip" | "bus" as kls, index] if index.isnumeric():
|
||||
target = getattr(self, kls)
|
||||
case [
|
||||
"vban",
|
||||
"in" | "instream" | "out" | "outstream" as direction,
|
||||
index,
|
||||
] if index.isnumeric():
|
||||
target = getattr(
|
||||
self.vban, f"{direction.removesuffix('stream')}stream"
|
||||
)
|
||||
case _:
|
||||
ERR_MSG = f"invalid config key '{key}'"
|
||||
self.logger.error(ERR_MSG)
|
||||
raise ValueError(ERR_MSG)
|
||||
return target[int(index)]
|
||||
|
||||
[target(key).apply(di).then_wait() for key, di in data.items()]
|
||||
|
||||
def apply_config(self, name):
|
||||
"""applies a config from memory"""
|
||||
ERR_MSG = (
|
||||
f"No config with name '{name}' is loaded into memory",
|
||||
f"Known configs: {list(self.configs.keys())}",
|
||||
)
|
||||
try:
|
||||
config = self.configs[name]
|
||||
except KeyError as e:
|
||||
self.logger.error(("\n").join(ERR_MSG))
|
||||
raise VBANCMDError(("\n").join(ERR_MSG)) from e
|
||||
|
||||
if "extends" in config:
|
||||
extended = config["extends"]
|
||||
config = {
|
||||
k: v
|
||||
for k, v in deep_merge(self.configs[extended], config)
|
||||
if k not in ("extends")
|
||||
}
|
||||
self.logger.debug(
|
||||
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||
)
|
||||
self.apply(config)
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
|
||||
def logout(self) -> None:
|
||||
if not self.stopped():
|
||||
self.logger.debug("events thread shutdown started")
|
||||
self.stop_event.set()
|
||||
for t in (self.producer, self.subscriber):
|
||||
t.join()
|
||||
[sock.close() for sock in self.socks]
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
||||
self.logout()
|
||||
@@ -1,66 +1,95 @@
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from .packet import (
|
||||
HEADER_SIZE,
|
||||
RegisterRTHeader,
|
||||
VBAN_VMRT_Packet_Data,
|
||||
VBAN_VMRT_Packet_Header,
|
||||
)
|
||||
from .error import VBANCMDConnectionError
|
||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
||||
from .util import Socket
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subscriber(threading.Thread):
|
||||
"""fire a subscription packet every 10 seconds"""
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="subscriber", target=self.register, daemon=True)
|
||||
self._rem = remote
|
||||
self.register_header = RegisterRTHeader()
|
||||
def __init__(self, remote, stop_event):
|
||||
super().__init__(name="subscriber", daemon=False)
|
||||
self._remote = remote
|
||||
self.stop_event = stop_event
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.packet = SubscribeHeader()
|
||||
|
||||
def register(self):
|
||||
while self._rem.running:
|
||||
def run(self):
|
||||
while not self.stopped():
|
||||
try:
|
||||
self._rem.socks[Socket.register].sendto(
|
||||
self.register_header.header,
|
||||
(socket.gethostbyname(self._rem.ip), self._rem.port),
|
||||
self._remote.socks[Socket.register].sendto(
|
||||
self.packet.header,
|
||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
||||
)
|
||||
count = int.from_bytes(self.register_header.framecounter, "little") + 1
|
||||
self.register_header.framecounter = count.to_bytes(4, "little")
|
||||
time.sleep(10)
|
||||
self.packet.framecounter = (
|
||||
int.from_bytes(self.packet.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
self.wait_until_stopped(10)
|
||||
except socket.gaierror as e:
|
||||
print(f"Unable to resolve hostname {self._rem.ip}")
|
||||
self._rem.socks[Socket.register].close()
|
||||
raise e
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDConnectionError(
|
||||
f"unable to resolve hostname {self._remote.ip}"
|
||||
) from e
|
||||
self.logger.debug(f"terminating {self.name} thread")
|
||||
|
||||
def stopped(self):
|
||||
return self.stop_event.is_set()
|
||||
|
||||
def wait_until_stopped(self, timeout, period=0.2):
|
||||
must_end = time.time() + timeout
|
||||
while time.time() < must_end:
|
||||
if self.stopped():
|
||||
break
|
||||
time.sleep(period)
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
"""
|
||||
continously updates the public packet
|
||||
class Producer(threading.Thread):
|
||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||
|
||||
notifies observers of event updates
|
||||
"""
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="updater", target=self.update, daemon=True)
|
||||
self._rem = remote
|
||||
self._rem.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._rem.port)
|
||||
def __init__(self, remote, queue, stop_event):
|
||||
super().__init__(name="producer", daemon=False)
|
||||
self._remote = remote
|
||||
self.queue = queue
|
||||
self.stop_event = stop_event
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.packet_expected = VbanRtPacketHeader()
|
||||
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
||||
self._remote.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||
)
|
||||
self.expected_packet = VBAN_VMRT_Packet_Header()
|
||||
self._rem._public_packet = self._get_rt()
|
||||
self._remote._public_packet = self._get_rt()
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
) = self._remote._get_levels(self._remote.public_packet)
|
||||
|
||||
def _fetch_rt_packet(self) -> Optional[VBAN_VMRT_Packet_Data]:
|
||||
"""Returns a valid RT Data Packet or None"""
|
||||
data, _ = self._rem.socks[Socket.response].recvfrom(2048)
|
||||
# check for packet data
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
def fget():
|
||||
data = None
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
return data
|
||||
|
||||
return fget()
|
||||
|
||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||
try:
|
||||
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
||||
# do we have packet data?
|
||||
if len(data) > HEADER_SIZE:
|
||||
# check if packet is of type VBAN
|
||||
if self.expected_packet.header == data[: HEADER_SIZE - 4]:
|
||||
return VBAN_VMRT_Packet_Data(
|
||||
# is the packet of type VBAN RT response?
|
||||
if self.packet_expected.header == data[:HEADER_SIZE]:
|
||||
return VbanRtPacket(
|
||||
_kind=self._remote.kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
@@ -84,40 +113,72 @@ class Updater(threading.Thread):
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
except TimeoutError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDConnectionError(
|
||||
f"timeout waiting for RtPacket from {self._remote.ip}"
|
||||
) from e
|
||||
|
||||
def _get_rt(self) -> VBAN_VMRT_Packet_Data:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
def stopped(self):
|
||||
return self.stop_event.is_set()
|
||||
|
||||
def fget():
|
||||
data = False
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
time.sleep(self._rem.DELAY)
|
||||
return data
|
||||
|
||||
return fget()
|
||||
|
||||
def update(self):
|
||||
print(f"Listening for {', '.join(self._rem.event.get())} events")
|
||||
(
|
||||
self._rem.cache["strip_level"],
|
||||
self._rem.cache["bus_level"],
|
||||
) = self._rem._get_levels(self._rem.public_packet)
|
||||
|
||||
while self._rem.running:
|
||||
start = time.time()
|
||||
def run(self):
|
||||
while not self.stopped():
|
||||
_pp = self._get_rt()
|
||||
self._rem._strip_buf, self._rem._bus_buf = self._rem._get_levels(_pp)
|
||||
self._rem._pdirty = _pp.pdirty(self._rem.public_packet)
|
||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||
ldirty = _pp.ldirty(
|
||||
self._remote.cache["strip_level"], self._remote.cache["bus_level"]
|
||||
)
|
||||
|
||||
if self._rem.event.ldirty and self._rem.ldirty:
|
||||
self._rem.cache["strip_level"] = self._rem._strip_buf
|
||||
self._rem.cache["bus_level"] = self._rem._bus_buf
|
||||
self._rem.subject.notify("ldirty")
|
||||
if self._rem.public_packet != _pp:
|
||||
self._rem._public_packet = _pp
|
||||
if self._rem.event.pdirty and self._rem.pdirty:
|
||||
self._rem.subject.notify("pdirty")
|
||||
elapsed = time.time() - start
|
||||
if self._rem.ratelimit - elapsed > 0:
|
||||
time.sleep(self._rem.ratelimit - elapsed)
|
||||
if pdirty or ldirty:
|
||||
self._remote._public_packet = _pp
|
||||
self._remote._pdirty = pdirty
|
||||
self._remote._ldirty = ldirty
|
||||
|
||||
if self._remote.event.pdirty:
|
||||
self.queue.put("pdirty")
|
||||
if self._remote.event.ldirty:
|
||||
self.queue.put("ldirty")
|
||||
time.sleep(self._remote.ratelimit)
|
||||
self.logger.debug(f"terminating {self.name} thread")
|
||||
self.queue.put(None)
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
"""
|
||||
continously updates the public packet
|
||||
|
||||
notifies observers of event updates
|
||||
"""
|
||||
|
||||
def __init__(self, remote, queue):
|
||||
super().__init__(name="updater", daemon=True)
|
||||
self._remote = remote
|
||||
self.queue = queue
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Continously update observers of dirty states.
|
||||
|
||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||
"""
|
||||
while event := self.queue.get():
|
||||
if event == "pdirty" and self._remote.pdirty:
|
||||
self._remote.subject.notify(event)
|
||||
elif event == "ldirty" and self._remote.ldirty:
|
||||
self._remote._strip_comp, self._remote._bus_comp = (
|
||||
self._remote._public_packet._strip_comp,
|
||||
self._remote._public_packet._bus_comp,
|
||||
)
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
) = (
|
||||
self._remote._public_packet.inputlevels,
|
||||
self._remote._public_packet.outputlevels,
|
||||
)
|
||||
self._remote.subject.notify(event)
|
||||
self.logger.debug(f"terminating {self.name} thread")
|
||||
|
||||
Reference in New Issue
Block a user