mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-07 08:03:30 +00:00
Compare commits
174 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a8ef82166c | |||
| 79f06ecc79 | |||
| b291c3a477 | |||
| c335d35b9f | |||
| 911d2f64a6 | |||
| e58d6c7242 | |||
| 870a95b41e | |||
| 59880bf582 | |||
| cc58d1f081 | |||
| e37dea38b3 | |||
| 7f3b0ac7c9 | |||
| 0512fac710 | |||
| d439da725c | |||
| 45ffed9f63 | |||
| 14f79d1388 | |||
| b45bd38706 | |||
| 312b5c5842 | |||
| ed8e281f7f | |||
| efdcfce387 | |||
| ad88286509 | |||
| ecbdd2778f | |||
| 1babf85a89 | |||
| fbd1d54f9b | |||
| 96e9d6f4fd | |||
| 51394c0076 | |||
| 91feccc509 | |||
| c9c365ac54 | |||
| 1742ff839e | |||
| 5299d9ec6b | |||
| bc2cd3e7a5 | |||
| af68c423a6 | |||
| 16df0d559e | |||
| dad5ee9e9d | |||
|
|
694e1036de | ||
| 8436634371 | |||
| 074ba4fe77 | |||
|
|
2b4e64ed76 | ||
| 21df4998a2 | |||
| 7bff293820 | |||
| c8d0a0078d | |||
| 87a1d62414 | |||
| f863723a4e | |||
| 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 |
53
.github/workflows/publish.yml
vendored
Normal file
53
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,53 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pip install poetry==2.3.1
|
||||
poetry --version
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
poetry install --only-root
|
||||
poetry build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
pypi-publish:
|
||||
needs: build
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/vban-cmd/
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./dist
|
||||
19
.github/workflows/ruff.yml
vendored
Normal file
19
.github/workflows/ruff.yml
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
name: Ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: 'format --check --diff'
|
||||
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/
|
||||
|
||||
# test files
|
||||
test-*.py
|
||||
|
||||
#config
|
||||
config.toml
|
||||
vban.toml
|
||||
|
||||
.vscode/
|
||||
159
CHANGELOG.md
159
CHANGELOG.md
@@ -11,6 +11,165 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.6.0] - 2026-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- support for packet with [ident:1](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982) in VBAN TEXT subprotocol.
|
||||
- This includes Strip 3D, PEQ, comp, gate, denoiser and pitch parameters.
|
||||
|
||||
## [2.5.2] - 2025-01-25
|
||||
|
||||
### Changed
|
||||
|
||||
- ip kwargs defaults to 'localhost'
|
||||
- bps kwarg defaults to 256000.
|
||||
- factory builder steps now logged at `DEBUG` level.
|
||||
- Internal socket changes, they don't affect interface usage.
|
||||
|
||||
## [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
|
||||
|
||||
360
README.md
360
README.md
@@ -1,14 +1,14 @@
|
||||
[](https://badge.fury.io/py/vban-cmd)
|
||||
[](https://github.com/onyx-and-iris/vban-cmd-python/blob/dev/LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
[](https://pycqa.github.io/isort/)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||

|
||||

|
||||
|
||||
# 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 +18,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
|
||||
|
||||
@@ -52,24 +63,28 @@ class ManyThings:
|
||||
self.vban = vban
|
||||
|
||||
def things(self):
|
||||
self.vban.strip[0].label = "podmic"
|
||||
self.vban.strip[0].label = 'podmic'
|
||||
self.vban.strip[0].mute = True
|
||||
print(
|
||||
f"strip 0 ({self.vban.strip[0].label}) mute has been set to {self.vban.strip[0].mute}"
|
||||
f'strip 0 ({self.vban.strip[0].label}) mute has been set to {self.vban.strip[0].mute}'
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
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))
|
||||
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}',
|
||||
)
|
||||
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()
|
||||
@@ -77,28 +92,22 @@ def main():
|
||||
# set many parameters at once
|
||||
vban.apply(
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
'strip-2': {'A1': True, 'B1': True, 'gain': -6.0},
|
||||
'bus-2': {'mute': True},
|
||||
'vban-in-0': {'on': True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
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 +125,116 @@ 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 `knob` is 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 `knob` is 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
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].eq.ab = True
|
||||
```
|
||||
|
||||
##### Strip.EQ.Channel.Cell
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `type`: int, from 0 up to 6
|
||||
- `f`: float, from 20.0 up to 20_000.0
|
||||
- `gain`: float, from -36.0 up to 18.0
|
||||
- `q`: float, from 0.3 up to 100
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].eq.channel[0].cell[2].on = True
|
||||
vban.strip[1].eq.channel[0].cell[2].f = 5000
|
||||
```
|
||||
|
||||
Strip EQ parameters are defined for PhysicalStrips, potato version only.
|
||||
|
||||
Only channel[0] properties are readable over VBAN.
|
||||
|
||||
##### Gainlayers
|
||||
|
||||
- `gain`: float, from -60.0 to 12.0
|
||||
@@ -158,8 +266,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 +273,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 +329,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 +374,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 +385,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 +395,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,64 +405,114 @@ 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 pass the following optional keyword arguments:
|
||||
You may also load a config that extends another config with overrides or additional parameters.
|
||||
|
||||
- `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
|
||||
You just need to define a key `extends` in the config TOML, that names the config to be extended.
|
||||
|
||||
#### Event updates
|
||||
Three example 'extender' configs are included with the repo. You may load them with:
|
||||
|
||||
To receive event updates you should do the following:
|
||||
```python
|
||||
import vban_cmd
|
||||
with vban_cmd.api('banana') as vm:
|
||||
vm.apply_config('extender')
|
||||
```
|
||||
|
||||
- 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.
|
||||
## Events
|
||||
|
||||
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.
|
||||
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
|
||||
# Listen for level updates
|
||||
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
"subs": {"ldirty": True},
|
||||
'ip': '<ip address>',
|
||||
'streamname': 'Command1',
|
||||
'port': 6980,
|
||||
}
|
||||
with vban_cmd.api('banana', **opts) as vban:
|
||||
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`
|
||||
|
||||
You may also add/remove event subscriptions as necessary with the Event class.
|
||||
Use the event class to toggle updates as necessary.
|
||||
|
||||
The following properties are available:
|
||||
|
||||
- `pdirty`: boolean
|
||||
- `ldirty`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.add("ldirty")
|
||||
vban.event.ldirty = True
|
||||
|
||||
vban.event.remove("pdirty")
|
||||
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='localhost', ip or hostname of remote machine
|
||||
- `port`: int=6980, vban udp port of remote machine.
|
||||
- `streamname`: str='Command1', name of the stream to connect to.
|
||||
- `bps`: int=256000, bps rate of the stream.
|
||||
- `channel`: int=0, channel on which to send the UDP requests.
|
||||
- `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`
|
||||
|
||||
True iff a parameter has been changed.
|
||||
@@ -342,24 +526,38 @@ True iff a level value has been changed.
|
||||
Sends a script block as a string request, for example:
|
||||
|
||||
```python
|
||||
vban.sendtext("Strip[0].Mute=1;Bus[0].Mono=1")
|
||||
vban.sendtext('Strip[0].Mute=1;Bus[0].Mono=1')
|
||||
```
|
||||
|
||||
#### `vban.public_packet`
|
||||
## Errors
|
||||
|
||||
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).
|
||||
- `errors.VBANCMDError`: Base VBANCMD Exception class.
|
||||
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
||||
|
||||
### `Errors`
|
||||
## Logging
|
||||
|
||||
- `errors.VMCMDErrors`: Base VMCMD error class.
|
||||
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||
|
||||
### `Tests`
|
||||
example:
|
||||
```python
|
||||
import vban_cmd
|
||||
|
||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
Then from tests directory:
|
||||
opts = {'ip': 'ip.local', 'port': 6980, 'streamname': 'Command1'}
|
||||
with vban_cmd.api('banana', **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
`pytest -v`
|
||||
### Run tests
|
||||
|
||||
Install [poetry](https://python-poetry.org/docs/#installation) and then:
|
||||
|
||||
```powershell
|
||||
poetry poe test-basic
|
||||
poetry poe test-banana
|
||||
poetry poe test-potato
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
37
__main__.py
37
__main__.py
@@ -6,24 +6,28 @@ class ManyThings:
|
||||
self.vban = vban
|
||||
|
||||
def things(self):
|
||||
self.vban.strip[0].label = "podmic"
|
||||
self.vban.strip[0].label = 'podmic'
|
||||
self.vban.strip[0].mute = True
|
||||
print(
|
||||
f"strip 0 ({self.vban.strip[0].label}) mute has been set to {self.vban.strip[0].mute}"
|
||||
f'strip 0 ({self.vban.strip[0].label}) mute has been set to {self.vban.strip[0].mute}'
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
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))
|
||||
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}',
|
||||
)
|
||||
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()
|
||||
@@ -31,21 +35,12 @@ def main():
|
||||
# set many parameters at once
|
||||
vban.apply(
|
||||
{
|
||||
"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"},
|
||||
'strip-2': {'A1': True, 'B1': True, 'gain': -6.0},
|
||||
'bus-2': {'mute': True},
|
||||
'vban-in-0': {'on': True},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
if __name__ == '__main__':
|
||||
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.
|
||||
117
examples/gui/__main__.py
Normal file
117
examples/gui/__main__.py
Normal file
@@ -0,0 +1,117 @@
|
||||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
|
||||
|
||||
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():
|
||||
KIND_ID = 'banana'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, ldirty=True, **conn) 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+.
|
||||
110
examples/obs/__main__.py
Normal file
110
examples/obs/__main__.py
Normal file
@@ -0,0 +1,110 @@
|
||||
import os
|
||||
import threading
|
||||
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',
|
||||
'propagate': False,
|
||||
}
|
||||
},
|
||||
'root': {'handlers': ['stream'], 'level': 'WARNING'},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vban, stop_event):
|
||||
self._vban = vban
|
||||
self._stop_event = stop_event
|
||||
self._client = obsws.EventClient()
|
||||
self._client.callback.register(
|
||||
(
|
||||
self.on_current_program_scene_changed,
|
||||
self.on_exit_started,
|
||||
)
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
self._client.disconnect()
|
||||
|
||||
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):
|
||||
scene = data.scene_name
|
||||
print(f'Switched to scene {scene}')
|
||||
match scene:
|
||||
case 'START':
|
||||
self.on_start()
|
||||
case 'BRB':
|
||||
self.on_brb()
|
||||
case 'END':
|
||||
self.on_end()
|
||||
case 'LIVE':
|
||||
self.on_live()
|
||||
|
||||
def on_exit_started(self, _):
|
||||
self._stop_event.set()
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = 'potato'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, **conn) as vban:
|
||||
stop_event = threading.Event()
|
||||
|
||||
with Observer(vban, stop_event):
|
||||
stop_event.wait()
|
||||
|
||||
|
||||
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,41 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
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":
|
||||
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))
|
||||
def on_update(self, event):
|
||||
if event == 'pdirty':
|
||||
print('pdirty!')
|
||||
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)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "potato"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
KIND_ID = 'banana'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True, **conn) as vban:
|
||||
App(vban)
|
||||
|
||||
while _ := input('Press <Enter> to exit\n'):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
513
poetry.lock
generated
513
poetry.lock
generated
@@ -1,294 +1,363 @@
|
||||
[[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.*"
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
description = "Classes Without Boilerplate"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.5"
|
||||
|
||||
[package.extras]
|
||||
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"]
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.6.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.2"
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
description = "Composable command line interface toolkit"
|
||||
category = "dev"
|
||||
name = "cachetools"
|
||||
version = "5.5.0"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "cachetools-5.5.0-py3-none-any.whl", hash = "sha256:02134e8439cdc2ffb62023ce1debca2944c3f289d66bb17ead3ab3dede74b292"},
|
||||
{file = "cachetools-5.5.0.tar.gz", hash = "sha256:2cc24fb4cbe39633fb7badd9db9ca6295d766d9c2995f245725a46715d050f2a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.2.0"
|
||||
description = "Universal encoding detector for Python 3"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
|
||||
{file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
|
||||
]
|
||||
|
||||
[[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"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.9"
|
||||
description = "Distribution utilities"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"},
|
||||
{file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "exceptiongroup"
|
||||
version = "1.2.2"
|
||||
description = "Backport of PEP 654 (exception groups)"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
|
||||
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
test = ["pytest (>=6)"]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.16.1"
|
||||
description = "A platform independent file lock."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "filelock-3.16.1-py3-none-any.whl", hash = "sha256:2082e5703d51fbf98ea75855d9d5527e33d8ff23099bec374a134febee6946b0"},
|
||||
{file = "filelock-3.16.1.tar.gz", hash = "sha256:c249fbfcd5db47e5e2d6d62198e565475ee65e4831e2561c8e313fa7eb961435"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.6.1)", "diff-cover (>=9.2)", "pytest (>=8.3.3)", "pytest-asyncio (>=0.24)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.26.4)"]
|
||||
typing = ["typing-extensions (>=4.12.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "1.1.1"
|
||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||
category = "dev"
|
||||
version = "2.0.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "5.10.1"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.1,<4.0"
|
||||
|
||||
[package.extras]
|
||||
pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||
requirements_deprecated_finder = ["pipreqs", "pip-api"]
|
||||
colors = ["colorama (>=0.4.3,<0.5.0)"]
|
||||
plugins = ["setuptools"]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-extensions"
|
||||
version = "0.4.3"
|
||||
description = "Experimental type system extensions for programs checked with the mypy typechecker."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
version = "24.2"
|
||||
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"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
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.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
version = "4.3.6"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
|
||||
{file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
|
||||
]
|
||||
|
||||
[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 (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
|
||||
type = ["mypy (>=1.11.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.0.0"
|
||||
version = "1.5.0"
|
||||
description = "plugin and hook calling mechanisms for python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
|
||||
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
version = "1.11.0"
|
||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||
category = "dev"
|
||||
name = "pyenv-inspect"
|
||||
version = "0.4.0"
|
||||
description = "An auxiliary library for the virtualenv-pyenv and tox-pyenv-redux plugins"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyenv-inspect-0.4.0.tar.gz", hash = "sha256:ec429d1d81b67ab0b08a0408414722a79d24fd1845a5b264267e44e19d8d60f0"},
|
||||
{file = "pyenv_inspect-0.4.0-py3-none-any.whl", hash = "sha256:618683ae7d3e6db14778d58aa0fc6b3170180d944669b5d35a8aa4fb7db550d2"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
category = "dev"
|
||||
name = "pyproject-api"
|
||||
version = "1.8.0"
|
||||
description = "API to interact with the python pyproject.toml based projects"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pyproject_api-1.8.0-py3-none-any.whl", hash = "sha256:3d7d347a047afe796fd5d1885b1e391ba29be7169bd2f102fcd378f04273d228"},
|
||||
{file = "pyproject_api-1.8.0.tar.gz", hash = "sha256:77b8049f2feb5d33eefcc21b57f1e279636277a8ac8ad6b5871037b243778496"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=24.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.2"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
py = ">=1.8.2"
|
||||
tomli = ">=1.0.0"
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-randomly"
|
||||
version = "3.12.0"
|
||||
version = "3.16.0"
|
||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.9"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest_randomly-3.16.0-py3-none-any.whl", hash = "sha256:8633d332635a1a0983d3bba19342196807f6afb17c3eef78e02c2f85dade45d6"},
|
||||
{file = "pytest_randomly-3.16.0.tar.gz", hash = "sha256:11bf4d23a26484de7860d82f726c0629837cf4064b79157bd18ec9d41d7feb26"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pytest = "*"
|
||||
|
||||
[[package]]
|
||||
name = "pytest-repeat"
|
||||
version = "0.9.1"
|
||||
description = "pytest plugin for repeating tests"
|
||||
category = "dev"
|
||||
name = "ruff"
|
||||
version = "0.9.2"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[package.dependencies]
|
||||
pytest = ">=3.6"
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.9.2-py3-none-linux_armv6l.whl", hash = "sha256:80605a039ba1454d002b32139e4970becf84b5fee3a3c3bf1c2af6f61a784347"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b9aab82bb20afd5f596527045c01e6ae25a718ff1784cb92947bff1f83068b00"},
|
||||
{file = "ruff-0.9.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fbd337bac1cfa96be615f6efcd4bc4d077edbc127ef30e2b8ba2a27e18c054d4"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b35259b0cbf8daa22a498018e300b9bb0174c2bbb7bcba593935158a78054d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b6a9701d1e371bf41dca22015c3f89769da7576884d2add7317ec1ec8cb9c3c"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9cc53e68b3c5ae41e8faf83a3b89f4a5d7b2cb666dff4b366bb86ed2a85b481f"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8efd9da7a1ee314b910da155ca7e8953094a7c10d0c0a39bfde3fcfd2a015684"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3292c5a22ea9a5f9a185e2d131dc7f98f8534a32fb6d2ee7b9944569239c648d"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1a605fdcf6e8b2d39f9436d343d1f0ff70c365a1e681546de0104bef81ce88df"},
|
||||
{file = "ruff-0.9.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c547f7f256aa366834829a08375c297fa63386cbe5f1459efaf174086b564247"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d18bba3d3353ed916e882521bc3e0af403949dbada344c20c16ea78f47af965e"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b338edc4610142355ccf6b87bd356729b62bf1bc152a2fad5b0c7dc04af77bfe"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:492a5e44ad9b22a0ea98cf72e40305cbdaf27fac0d927f8bc9e1df316dcc96eb"},
|
||||
{file = "ruff-0.9.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:af1e9e9fe7b1f767264d26b1075ac4ad831c7db976911fa362d09b2d0356426a"},
|
||||
{file = "ruff-0.9.2-py3-none-win32.whl", hash = "sha256:71cbe22e178c5da20e1514e1e01029c73dc09288a8028a5d3446e6bba87a5145"},
|
||||
{file = "ruff-0.9.2-py3-none-win_amd64.whl", hash = "sha256:c5e1d6abc798419cf46eed03f54f2e0c3adb1ad4b801119dedf23fcaf69b55b5"},
|
||||
{file = "ruff-0.9.2-py3-none-win_arm64.whl", hash = "sha256:a1b63fa24149918f8b37cef2ee6fff81f24f0d74b6f0bdc37bc3e1f2143e41c6"},
|
||||
{file = "ruff-0.9.2.tar.gz", hash = "sha256:b5eceb334d55fae5f316f783437392642ae18e16dcf4f1858d55d3c2a0f8f5d0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
version = "2.2.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"},
|
||||
{file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"},
|
||||
{file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"},
|
||||
{file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"},
|
||||
{file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"},
|
||||
{file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.23.2"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "tox-4.23.2-py3-none-any.whl", hash = "sha256:452bc32bb031f2282881a2118923176445bac783ab97c874b8770ab4c3b76c38"},
|
||||
{file = "tox-4.23.2.tar.gz", hash = "sha256:86075e00e555df6e82e74cfc333917f91ecb47ffbc868dcafbd2672e332f4a2c"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=5.5"
|
||||
chardet = ">=5.2"
|
||||
colorama = ">=0.4.6"
|
||||
filelock = ">=3.16.1"
|
||||
packaging = ">=24.1"
|
||||
platformdirs = ">=4.3.6"
|
||||
pluggy = ">=1.5"
|
||||
pyproject-api = ">=1.8"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""}
|
||||
virtualenv = ">=20.26.6"
|
||||
|
||||
[package.extras]
|
||||
test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.12.2"
|
||||
description = "Backported and Experimental Type Hints for Python 3.8+"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
markers = "python_version < \"3.11\""
|
||||
files = [
|
||||
{file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
|
||||
{file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.29.0"
|
||||
description = "Virtual Python Environment builder"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "virtualenv-20.29.0-py3-none-any.whl", hash = "sha256:c12311863497992dc4b8644f8ea82d3b35bb7ef8ee82e6630d76d0197c39baf9"},
|
||||
{file = "virtualenv-20.29.0.tar.gz", hash = "sha256:6345e1ff19d4b1296954cee076baaf58ff2a12a84a338c62b02eda39f20aa982"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.7,<1"
|
||||
filelock = ">=3.12.2,<4"
|
||||
platformdirs = ">=3.9.1,<5"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv-pyenv"
|
||||
version = "0.5.0"
|
||||
description = "A virtualenv Python discovery plugin for pyenv-installed interpreters"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "virtualenv-pyenv-0.5.0.tar.gz", hash = "sha256:7b0e5fe3dfbdf484f4cf9b01e1f98111e398db6942237910f666356e6293597f"},
|
||||
{file = "virtualenv_pyenv-0.5.0-py3-none-any.whl", hash = "sha256:21750247e36c55b3c547cfdeb08f51a3867fe7129922991a4f9c96980c0a4a5d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pyenv-inspect = ">=0.4,<0.5"
|
||||
virtualenv = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014"
|
||||
|
||||
[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"},
|
||||
]
|
||||
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"},
|
||||
]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10"
|
||||
content-hash = "13fc9d0eb15d5fc09b54c1c8cd8f528b260259e97ee6813b50ab4724c35d6677"
|
||||
|
||||
145
pyproject.toml
145
pyproject.toml
@@ -1,23 +1,138 @@
|
||||
[tool.poetry]
|
||||
[project]
|
||||
name = "vban-cmd"
|
||||
version = "1.3.2"
|
||||
version = "2.6.0"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||
license = { text = "MIT" }
|
||||
readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/vban-cmd-python"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["tomli (>=2.0.1,<3.0) ; python_version < '3.11'"]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
[tool.poetry.requires-plugins]
|
||||
poethepoet = "^0.35.0"
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
pytest-randomly = "^3.12.0"
|
||||
pytest-repeat = "^0.9.1"
|
||||
black = "^22.3.0"
|
||||
isort = "^5.10.1"
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.4"
|
||||
pytest-randomly = "^3.16.0"
|
||||
ruff = "^0.9.2"
|
||||
tox = "^4.23.2"
|
||||
virtualenv-pyenv = "^0.5.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe]
|
||||
envfile = ".env"
|
||||
|
||||
[tool.poe.tasks]
|
||||
gui.script = "scripts:ex_gui"
|
||||
obs.script = "scripts:ex_obs"
|
||||
observer.script = "scripts:ex_observer"
|
||||
test-basic.script = "scripts:test_basic"
|
||||
test-banana.script = "scripts:test_banana"
|
||||
test-potato.script = "scripts:test_potato"
|
||||
test-all.script = "scripts:test_all"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310,py311,py312,py313
|
||||
|
||||
[testenv]
|
||||
passenv = *
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
commands =
|
||||
poetry install -v
|
||||
poetry run pytest tests/
|
||||
|
||||
|
||||
[testenv:obs]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps = obsws-python
|
||||
commands =
|
||||
poetry install -v --without dev
|
||||
poetry run python examples/obs/
|
||||
"""
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
".bzr",
|
||||
".direnv",
|
||||
".eggs",
|
||||
".git",
|
||||
".git-rewrite",
|
||||
".hg",
|
||||
".mypy_cache",
|
||||
".nox",
|
||||
".pants.d",
|
||||
".pytype",
|
||||
".ruff_cache",
|
||||
".svn",
|
||||
".tox",
|
||||
".venv",
|
||||
"__pypackages__",
|
||||
"_build",
|
||||
"buck-out",
|
||||
"build",
|
||||
"dist",
|
||||
"node_modules",
|
||||
"venv",
|
||||
]
|
||||
|
||||
# Same as Black.
|
||||
line-length = 88
|
||||
indent-width = 4
|
||||
|
||||
# Assume Python 3.10
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
|
||||
[tool.ruff.format]
|
||||
# Unlike Black, use single quotes for strings.
|
||||
quote-style = "single"
|
||||
|
||||
# Like Black, indent with spaces, rather than tabs.
|
||||
indent-style = "space"
|
||||
|
||||
# Like Black, respect magic trailing commas.
|
||||
skip-magic-trailing-comma = false
|
||||
|
||||
# Like Black, automatically detect the appropriate line ending.
|
||||
line-ending = "auto"
|
||||
|
||||
# Enable auto-formatting of code examples in docstrings. Markdown,
|
||||
# reStructuredText code/literal blocks and doctests are all supported.
|
||||
#
|
||||
# This is currently disabled by default, but it is planned for this
|
||||
# to be opt-out in the future.
|
||||
docstring-code-format = false
|
||||
|
||||
# Set the line length limit used when formatting code snippets in
|
||||
# docstrings.
|
||||
#
|
||||
# This only has an effect when the `docstring-code-format` setting is
|
||||
# enabled.
|
||||
docstring-code-line-length = "dynamic"
|
||||
|
||||
[tool.ruff.lint.mccabe]
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402", "F401"]
|
||||
|
||||
35
scripts.py
Normal file
35
scripts.py
Normal file
@@ -0,0 +1,35 @@
|
||||
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():
|
||||
subprocess.run(['tox', 'r', '-e', 'obs'])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
scriptpath = Path.cwd() / 'examples' / 'observer' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def test_basic():
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'basic'})
|
||||
|
||||
|
||||
def test_banana():
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'banana'})
|
||||
|
||||
|
||||
def test_potato():
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'potato'})
|
||||
|
||||
|
||||
def test_all():
|
||||
steps = [test_basic, test_banana, test_potato]
|
||||
[step() for step in steps]
|
||||
@@ -1,25 +1,23 @@
|
||||
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 from environment, if not set default to potato
|
||||
KIND_ID = os.environ.get('KIND', 'potato')
|
||||
|
||||
opts = {
|
||||
"ip": "ws.local",
|
||||
"streamname": "workstation",
|
||||
"port": 6990,
|
||||
"bps": 0,
|
||||
"sync": True,
|
||||
'ip': os.getenv('VBANCMD_IP', 'localhost'),
|
||||
'streamname': os.getenv('VBANCMD_STREAMNAME', 'Command1'),
|
||||
'port': int(os.getenv('VBANCMD_PORT', 6980)),
|
||||
}
|
||||
|
||||
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
|
||||
@@ -41,10 +39,10 @@ data = Data()
|
||||
|
||||
|
||||
def setup_module():
|
||||
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
||||
tests.login()
|
||||
tests.command.reset()
|
||||
print(f'\nRunning tests for kind [{data.name}]\n', file=sys.stdout)
|
||||
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,7 +1,7 @@
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
"--run-slow",
|
||||
action="store_true",
|
||||
'--run-slow',
|
||||
action='store_true',
|
||||
default=False,
|
||||
help="Run slow tests",
|
||||
help='Run slow tests',
|
||||
)
|
||||
|
||||
@@ -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")
|
||||
|
||||
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
|
||||
|
||||
def test_it_tests_config_bool(self):
|
||||
assert tests.strip[0].A1 == True
|
||||
vban.apply_config('example')
|
||||
time.sleep(0.1)
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
reason='Only run when --run-slow is given',
|
||||
)
|
||||
def test_it_tests_config_string(self):
|
||||
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 vban.strip[0].A1
|
||||
|
||||
@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 vban
|
||||
|
||||
|
||||
class TestErrors:
|
||||
__test__ = True
|
||||
|
||||
def test_it_tests_an_unknown_kind(self):
|
||||
with pytest.raises(
|
||||
vban_cmd.error.VBANCMDError,
|
||||
match="Unknown Voicemeeter kind 'unknown_kind'",
|
||||
):
|
||||
vban_cmd.api('unknown_kind')
|
||||
|
||||
def test_it_tests_an_unknown_config_name(self):
|
||||
EXPECTED_MSG = '\n'.join(
|
||||
(
|
||||
"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,43 +1,55 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestRemoteFactories:
|
||||
__test__ = True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "basic",
|
||||
reason="Skip test if kind is not basic",
|
||||
data.name != 'basic',
|
||||
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",
|
||||
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",
|
||||
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,82 +1,80 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [False, True])
|
||||
@pytest.mark.parametrize('value', [False, True])
|
||||
class TestSetAndGetBoolHigher:
|
||||
__test__ = True
|
||||
|
||||
"""strip tests, physical and virtual"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param",
|
||||
'index,param',
|
||||
[
|
||||
(data.phys_in, "mute"),
|
||||
(data.virt_in, "solo"),
|
||||
(data.phys_in, 'mute'),
|
||||
(data.virt_in, 'solo'),
|
||||
],
|
||||
)
|
||||
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",
|
||||
reason="Only test if logged into Basic or Potato version",
|
||||
data.name == 'banana',
|
||||
reason='Only test if logged into Basic or Potato version',
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index,param",
|
||||
'index,param',
|
||||
[
|
||||
(data.phys_in, "mc"),
|
||||
(data.phys_in, 'mc'),
|
||||
],
|
||||
)
|
||||
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",
|
||||
'index,param',
|
||||
[
|
||||
(data.phys_out, "eq"),
|
||||
(data.phys_out, "mute"),
|
||||
(data.virt_out, "eq_ab"),
|
||||
(data.virt_out, "sel"),
|
||||
(data.phys_out, 'mute'),
|
||||
],
|
||||
)
|
||||
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 """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param",
|
||||
'index,param',
|
||||
[
|
||||
(data.phys_out, "normal"),
|
||||
(data.phys_out, "amix"),
|
||||
(data.phys_out, "rearonly"),
|
||||
(data.virt_out, "normal"),
|
||||
(data.virt_out, "upmix41"),
|
||||
(data.virt_out, "composite"),
|
||||
(data.phys_out, 'normal'),
|
||||
(data.phys_out, 'amix'),
|
||||
(data.phys_out, 'rearonly'),
|
||||
(data.virt_out, 'normal'),
|
||||
(data.virt_out, 'upmix41'),
|
||||
(data.virt_out, 'composite'),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
||||
def test_it_sets_and_gets_bus_mode_bool_params(self, index, param, value):
|
||||
# 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 """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"param",
|
||||
[("lock")],
|
||||
'param',
|
||||
[('lock')],
|
||||
)
|
||||
def test_it_sets_command_bool_params(self, param, value):
|
||||
setattr(tests.command, param, value)
|
||||
setattr(vban.command, param, value)
|
||||
|
||||
|
||||
class TestSetAndGetIntHigher:
|
||||
@@ -87,15 +85,15 @@ class TestSetAndGetIntHigher:
|
||||
"""strip tests, physical and virtual"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param,value",
|
||||
'index,param,value',
|
||||
[
|
||||
(data.virt_in, "k", 0),
|
||||
(data.virt_in, "k", 4),
|
||||
(data.virt_in, 'k', 0),
|
||||
(data.virt_in, 'k', 4),
|
||||
],
|
||||
)
|
||||
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:
|
||||
@@ -104,31 +102,33 @@ class TestSetAndGetFloatHigher:
|
||||
"""strip tests, physical and virtual"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param,value",
|
||||
'index,param,value',
|
||||
[
|
||||
(data.phys_in, "gain", -3.6),
|
||||
(data.phys_in, "gain", 3.6),
|
||||
(data.virt_in, "gain", -5.8),
|
||||
(data.virt_in, "gain", 5.8),
|
||||
(data.phys_in, 'gain', -3.6),
|
||||
(data.phys_in, 'gain', 3.6),
|
||||
(data.virt_in, 'gain', -5.8),
|
||||
(data.virt_in, 'gain', 5.8),
|
||||
],
|
||||
)
|
||||
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",
|
||||
'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
|
||||
def test_it_gets_strip_prefader_levels_and_compares_length_of_array(
|
||||
self, index, value
|
||||
):
|
||||
assert len(vban.strip[index].levels.prefader) == value
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Only test if logged into Potato version",
|
||||
data.name != 'potato',
|
||||
reason='Only test if logged into Potato version',
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index, j, value",
|
||||
'index, j, value',
|
||||
[
|
||||
(data.phys_in, 0, -20.7),
|
||||
(data.virt_in, 3, -60),
|
||||
@@ -137,61 +137,96 @@ 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 """
|
||||
|
||||
@pytest.mark.skip(reason='Requires RT Packet NBS 1')
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
'index, param, value',
|
||||
[
|
||||
(data.virt_in, "treble", -1.6),
|
||||
(data.virt_in, "mid", 5.8),
|
||||
(data.virt_in, "bass", -8.1),
|
||||
(data.virt_in, 'treble', -1.6),
|
||||
(data.virt_in, 'mid', 5.8),
|
||||
(data.virt_in, 'bass', -8.1),
|
||||
],
|
||||
)
|
||||
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 """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
||||
'index, param, value',
|
||||
[(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",
|
||||
'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
|
||||
def test_it_gets_bus_levels_and_compares_length_of_array(self, index, value):
|
||||
assert len(vban.bus[index].levels.all) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["test0", "test1"])
|
||||
@pytest.mark.parametrize('value', ['test0', 'test1'])
|
||||
class TestSetAndGetStringHigher:
|
||||
__test__ = True
|
||||
|
||||
"""strip tests, physical and virtual"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index, param",
|
||||
[(data.phys_in, "label"), (data.virt_in, "label")],
|
||||
'index, param',
|
||||
[(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 """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index, param",
|
||||
[(data.phys_out, "label"), (data.virt_out, "label")],
|
||||
'index, param',
|
||||
[(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:
|
||||
@@ -11,31 +9,26 @@ class TestPublicPacketLower:
|
||||
|
||||
"""Tests for a valid rt data packet"""
|
||||
|
||||
def test_it_gets_an_rt_data_packet(self):
|
||||
assert tests.public_packet.voicemeetertype in (
|
||||
kind.name for kind in kinds.kinds_all
|
||||
def test_it_gets_an_rt0_data_packet(self):
|
||||
assert vban.public_packets[0].voicemeetertype in (
|
||||
kind.name for kind in kinds.all
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
@pytest.mark.parametrize("value", [0, 1])
|
||||
@pytest.mark.parametrize('value', [0, 1])
|
||||
class TestSetRT:
|
||||
__test__ = True
|
||||
|
||||
"""Tests set_rt"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"kls,index,param",
|
||||
'kls,index,param',
|
||||
[
|
||||
("strip", data.phys_in, "mute"),
|
||||
("bus", data.virt_out, "mono"),
|
||||
('strip', data.phys_in, 'mute'),
|
||||
('bus', data.virt_out, 'mono'),
|
||||
],
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
from .factory import request_vbancmd_obj as api
|
||||
|
||||
__ALL__ = ["api"]
|
||||
__ALL__ = ['api']
|
||||
|
||||
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()
|
||||
144
vban_cmd/bus.py
144
vban_cmd/bus.py
@@ -1,17 +1,11 @@
|
||||
import abc
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from typing import Union
|
||||
|
||||
from .enums import NBS, BusModes
|
||||
from .iremote import IRemote
|
||||
from .meta import bus_mode_prop, channel_bool_prop, channel_label_prop
|
||||
|
||||
BusModes = IntEnum(
|
||||
"BusModes",
|
||||
"normal amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly",
|
||||
start=0,
|
||||
)
|
||||
|
||||
|
||||
class Bus(IRemote):
|
||||
"""
|
||||
@@ -20,38 +14,55 @@ class Bus(IRemote):
|
||||
Defines concrete implementation for bus
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@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
|
||||
val = self.getter('gain')
|
||||
if val:
|
||||
return round(val, 2)
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
|
||||
val = self.getter("gain")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return self.public_packets[NBS.zero].busgain[self.index]
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
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):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
@property
|
||||
def device(self) -> str:
|
||||
@@ -64,7 +75,7 @@ class PhysicalBus(Bus):
|
||||
|
||||
class VirtualBus(Bus):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
|
||||
class BusLevel(IRemote):
|
||||
@@ -79,14 +90,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)
|
||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-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_packets[NBS.zero])[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:
|
||||
@@ -107,37 +128,47 @@ class BusLevel(IRemote):
|
||||
def _make_bus_mode_mixin():
|
||||
"""Creates a mixin of Bus Modes."""
|
||||
|
||||
modestates = {
|
||||
'normal': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
'amix': [1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1],
|
||||
'repeat': [0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2],
|
||||
'bmix': [1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3],
|
||||
'composite': [0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0],
|
||||
'tvmix': [1, 0, 1, 4, 5, 4, 5, 0, 1, 0, 1],
|
||||
'upmix21': [0, 2, 2, 4, 4, 6, 6, 0, 0, 2, 2],
|
||||
'upmix41': [1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3],
|
||||
'upmix61': [0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8],
|
||||
'centeronly': [1, 0, 1, 0, 1, 0, 1, 8, 9, 8, 9],
|
||||
'lfeonly': [0, 2, 2, 0, 0, 2, 2, 8, 8, 10, 10],
|
||||
'rearonly': [1, 2, 3, 0, 1, 2, 3, 8, 9, 10, 11],
|
||||
}
|
||||
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}].mode"
|
||||
return f'bus[{self.index}].mode'
|
||||
|
||||
def get(self):
|
||||
time.sleep(0.01)
|
||||
for i, val in enumerate(
|
||||
[
|
||||
self.amix,
|
||||
self.bmix,
|
||||
self.repeat,
|
||||
self.composite,
|
||||
self.tvmix,
|
||||
self.upmix21,
|
||||
self.upmix41,
|
||||
self.upmix61,
|
||||
self.centeronly,
|
||||
self.lfeonly,
|
||||
self.rearonly,
|
||||
states = [
|
||||
(
|
||||
int.from_bytes(
|
||||
self.public_packets[NBS.zero].busstate[self.index], 'little'
|
||||
)
|
||||
& val
|
||||
)
|
||||
>> 4
|
||||
for val in self._modes.modevals
|
||||
]
|
||||
):
|
||||
if val:
|
||||
return BusModes(i + 1).name
|
||||
return "normal"
|
||||
for k, v in modestates.items():
|
||||
if states == v:
|
||||
return k
|
||||
|
||||
return type(
|
||||
"BusModeMixin",
|
||||
'BusModeMixin',
|
||||
(IRemote,),
|
||||
{
|
||||
"identifier": property(identifier),
|
||||
'identifier': property(identifier),
|
||||
'modestates': modestates,
|
||||
**{mode.name: bus_mode_prop(mode.name) for mode in BusModes},
|
||||
"get": get,
|
||||
'get': get,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -151,15 +182,14 @@ def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
BUS_cls = PhysicalBus if phys_bus else VirtualBus
|
||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||
return type(
|
||||
f"{BUS_cls.__name__}{remote.kind}",
|
||||
f'{BUS_cls.__name__}{remote.kind}',
|
||||
(BUS_cls,),
|
||||
{
|
||||
"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(),
|
||||
'eq': BusEQ.make(remote, i),
|
||||
'levels': BusLevel(remote, i),
|
||||
'mode': BUSMODEMIXIN_cls(remote, i),
|
||||
**{param: channel_bool_prop(param) for param in ['mute', 'mono']},
|
||||
'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):
|
||||
@@ -18,31 +17,30 @@ class Command(IRemote):
|
||||
Returns a Command class of a kind.
|
||||
"""
|
||||
CMD_cls = type(
|
||||
f"Command{remote.kind}",
|
||||
f'Command{remote.kind}',
|
||||
(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)
|
||||
self.setter('DialogShow.VBANCHAT', 1 if val else 0)
|
||||
|
||||
showvbanchat = property(fset=set_showvbanchat)
|
||||
|
||||
def set_lock(self, val: bool):
|
||||
self.setter("lock", 1 if val else 0)
|
||||
self.setter('lock', 1 if val else 0)
|
||||
|
||||
lock = property(fset=set_lock)
|
||||
|
||||
def reset(self):
|
||||
self._remote.apply_config("reset")
|
||||
self._remote.apply_config('reset')
|
||||
|
||||
@@ -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"""
|
||||
@@ -12,66 +20,73 @@ class TOMLStrBuilder:
|
||||
def __init__(self, kind):
|
||||
self.kind = kind
|
||||
self.higher = itertools.chain(
|
||||
[f"strip-{i}" for i in range(kind.num_strip)],
|
||||
[f"bus-{i}" for i in range(kind.num_bus)],
|
||||
[f'strip-{i}' for i in range(kind.num_strip)],
|
||||
[f'bus-{i}' for i in range(kind.num_bus)],
|
||||
)
|
||||
|
||||
def init_config(self, profile=None):
|
||||
self.virt_strip_params = (
|
||||
[
|
||||
"mute = false",
|
||||
"mono = false",
|
||||
"solo = false",
|
||||
"gain = 0.0",
|
||||
'mute = false',
|
||||
'mono = false',
|
||||
'solo = false',
|
||||
'gain = 0.0',
|
||||
]
|
||||
+ [f"A{i} = false" for i in range(1, self.kind.phys_out + 1)]
|
||||
+ [f"B{i} = false" for i in range(1, self.kind.virt_out + 1)]
|
||||
+ [f'A{i} = false' for i in range(1, self.kind.phys_out + 1)]
|
||||
+ [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":
|
||||
if profile == 'reset':
|
||||
self.reset_config()
|
||||
|
||||
def reset_config(self):
|
||||
self.phys_strip_params = list(
|
||||
map(lambda x: x.replace("B1 = false", "B1 = true"), self.phys_strip_params)
|
||||
map(lambda x: x.replace('B1 = false', 'B1 = true'), self.phys_strip_params)
|
||||
)
|
||||
self.virt_strip_params = list(
|
||||
map(lambda x: x.replace("A1 = false", "A1 = true"), self.virt_strip_params)
|
||||
map(lambda x: x.replace('A1 = false', 'A1 = true'), self.virt_strip_params)
|
||||
)
|
||||
|
||||
def build(self, profile="reset"):
|
||||
def build(self, profile='reset'):
|
||||
self.init_config(profile)
|
||||
toml_str = str()
|
||||
for eachclass in self.higher:
|
||||
toml_str += f"[{eachclass}]\n"
|
||||
toml_str += f'[{eachclass}]\n'
|
||||
toml_str = self.join(eachclass, toml_str)
|
||||
return toml_str
|
||||
|
||||
def join(self, eachclass, toml_str):
|
||||
kls, index = eachclass.split("-")
|
||||
kls, index = eachclass.split('-')
|
||||
match kls:
|
||||
case "strip":
|
||||
toml_str += ("\n").join(
|
||||
case 'strip':
|
||||
toml_str += ('\n').join(
|
||||
self.phys_strip_params
|
||||
if int(index) < self.kind.phys_in
|
||||
else self.virt_strip_params
|
||||
)
|
||||
case "bus":
|
||||
toml_str += ("\n").join(self.bus_bool)
|
||||
case 'bus':
|
||||
toml_str += ('\n').join(self.bus_params)
|
||||
case _:
|
||||
pass
|
||||
return toml_str + "\n"
|
||||
return toml_str + '\n'
|
||||
|
||||
|
||||
class TOMLDataExtractor:
|
||||
def __init__(self, file):
|
||||
self._data = dict()
|
||||
with open(file, "rb") as f:
|
||||
with open(file, 'rb') as f:
|
||||
self._data = tomllib.load(f)
|
||||
|
||||
@property
|
||||
@@ -89,10 +104,10 @@ def dataextraction_factory(file):
|
||||
|
||||
this opens the possibility for other parsers to be added
|
||||
"""
|
||||
if file.suffix == ".toml":
|
||||
if file.suffix == '.toml':
|
||||
extractor = TOMLDataExtractor
|
||||
else:
|
||||
raise ValueError("Cannot extract data from {}".format(file))
|
||||
raise ValueError('Cannot extract data from {}'.format(file))
|
||||
return extractor(file)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -125,18 +141,25 @@ class Loader(metaclass=SingletonType):
|
||||
def defaults(self, kind):
|
||||
self.builder = TOMLStrBuilder(kind)
|
||||
toml_str = self.builder.build()
|
||||
self.register("reset", tomllib.loads(toml_str))
|
||||
self.register('reset', tomllib.loads(toml_str))
|
||||
|
||||
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,17 +182,18 @@ 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.cwd() / '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:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
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):
|
||||
loader.register(identifier)
|
||||
return loader.configs
|
||||
@@ -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
|
||||
|
||||
20
vban_cmd/enums.py
Normal file
20
vban_cmd/enums.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from enum import Enum, IntEnum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
BASIC = 1
|
||||
BANANA = 2
|
||||
POTATO = 3
|
||||
|
||||
|
||||
class NBS(IntEnum):
|
||||
zero = 0
|
||||
one = 1
|
||||
|
||||
|
||||
BusModes = IntEnum(
|
||||
'BusModes',
|
||||
'normal amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly',
|
||||
start=0,
|
||||
)
|
||||
@@ -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 += ('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 @@
|
||||
from abc import abstractmethod
|
||||
import abc
|
||||
import logging
|
||||
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,58 +25,73 @@ 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
|
||||
self.kind = kind
|
||||
self._info = (
|
||||
f"Finished building strips for {self._factory}",
|
||||
f"Finished building buses for {self._factory}",
|
||||
f"Finished building commands for {self._factory}",
|
||||
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))])
|
||||
name = name.split('_')[1]
|
||||
self.logger.debug(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,
|
||||
"streamname": "Command1",
|
||||
"bps": 0,
|
||||
"channel": 0,
|
||||
"ratelimit": 0.01,
|
||||
"sync": False,
|
||||
"subs": defaultsubs,
|
||||
'ip': 'localhost',
|
||||
'port': 6980,
|
||||
'streamname': 'Command1',
|
||||
'bps': 256000,
|
||||
'channel': 0,
|
||||
'ratelimit': 0.01,
|
||||
'timeout': 5,
|
||||
'outbound': False,
|
||||
'sync': False,
|
||||
'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,14 +100,22 @@ 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}"
|
||||
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
|
||||
@abc.abstractmethod
|
||||
def steps(self):
|
||||
pass
|
||||
|
||||
@@ -169,15 +198,15 @@ def vbancmd_factory(kind_id: str, **kwargs) -> VbanCmd:
|
||||
Returns a VbanCmd class of a kind
|
||||
"""
|
||||
match kind_id:
|
||||
case "basic":
|
||||
case 'basic':
|
||||
_factory = BasicFactory
|
||||
case "banana":
|
||||
case 'banana':
|
||||
_factory = BananaFactory
|
||||
case "potato":
|
||||
case 'potato':
|
||||
_factory = PotatoFactory
|
||||
case _:
|
||||
raise ValueError(f"Unknown Voicemeeter kind '{kind_id}'")
|
||||
return type(f"VbanCmd{kind_id.capitalize()}", (_factory,), {})(kind_id, **kwargs)
|
||||
return type(f'VbanCmd{kind_id.capitalize()}', (_factory,), {})(kind_id, **kwargs)
|
||||
|
||||
|
||||
def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
||||
@@ -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 abc
|
||||
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
|
||||
@@ -75,7 +78,7 @@ class Modes:
|
||||
)
|
||||
|
||||
|
||||
class IRemote(metaclass=ABCMeta):
|
||||
class IRemote(abc.ABC):
|
||||
"""
|
||||
Common interface between base class and extended (higher) classes
|
||||
|
||||
@@ -85,46 +88,64 @@ 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)
|
||||
|
||||
@abstractmethod
|
||||
def _cmd(self, param):
|
||||
cmd = (self.identifier,)
|
||||
if param:
|
||||
cmd += (f'.{param}',)
|
||||
return ''.join(cmd)
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def public_packet(self):
|
||||
def public_packets(self):
|
||||
"""Returns an RT data packet."""
|
||||
return self._remote.public_packet
|
||||
return self._remote.public_packets
|
||||
|
||||
def apply(self, data):
|
||||
"""Sets all parameters of a dict for the channel."""
|
||||
|
||||
def fget(attr, val):
|
||||
if attr == "mode":
|
||||
return (f"mode.{val}", 1)
|
||||
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,12 +1,7 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
BASIC = 1
|
||||
BANANA = 2
|
||||
POTATO = 3
|
||||
from .enums import KindId
|
||||
from .error import VBANCMDError
|
||||
|
||||
|
||||
class SingletonType(type):
|
||||
@@ -20,12 +15,15 @@ class SingletonType(type):
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class KindMapClass(metaclass=SingletonType):
|
||||
name: str
|
||||
ins: tuple
|
||||
outs: tuple
|
||||
vban: tuple
|
||||
strip_channels: int
|
||||
bus_channels: int
|
||||
cells: int
|
||||
|
||||
@property
|
||||
def phys_in(self):
|
||||
@@ -51,44 +49,61 @@ 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()
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4)
|
||||
vban: tuple = (4, 4, 1, 1)
|
||||
strip_channels: int = 0
|
||||
bus_channels: int = 0
|
||||
cells: int = 0
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
strip_channels: int = 0
|
||||
bus_channels: int = 8
|
||||
cells: int = 6
|
||||
|
||||
|
||||
@dataclass
|
||||
@dataclass(frozen=True)
|
||||
class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
strip_channels: int = 2
|
||||
bus_channels: int = 8
|
||||
cells: int = 6
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
match kind_id:
|
||||
case "basic":
|
||||
case 'basic':
|
||||
_kind_map = BasicMap
|
||||
case "banana":
|
||||
case 'banana':
|
||||
_kind_map = BananaMap
|
||||
case "potato":
|
||||
case 'potato':
|
||||
_kind_map = PotatoMap
|
||||
case _:
|
||||
raise ValueError(f"Unknown Voicemeeter kind {kind_id}")
|
||||
raise ValueError(f'Unknown Voicemeeter kind {kind_id}')
|
||||
return _kind_map(name=kind_id)
|
||||
|
||||
|
||||
@@ -97,8 +112,8 @@ 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
|
||||
|
||||
|
||||
kinds_all = list(request_kind_map(kind_id.name.lower()) for kind_id in KindId)
|
||||
all = list(request_kind_map(kind_id.name.lower()) for kind_id in KindId)
|
||||
|
||||
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')
|
||||
122
vban_cmd/meta.py
122
vban_cmd/meta.py
@@ -1,7 +1,7 @@
|
||||
from functools import partial
|
||||
|
||||
from .error import VMCMDErrors
|
||||
from .util import cache_bool, cache_string
|
||||
from .enums import NBS
|
||||
from .util import cache_bool, cache_float, cache_string
|
||||
|
||||
|
||||
def channel_bool_prop(param):
|
||||
@@ -9,15 +9,17 @@ def channel_bool_prop(param):
|
||||
|
||||
@partial(cache_bool, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
return (
|
||||
not int.from_bytes(
|
||||
getattr(
|
||||
self.public_packet,
|
||||
f"{'strip' if 'strip' in type(self).__name__.lower() else 'bus'}state",
|
||||
self.public_packets[NBS.zero],
|
||||
f'{"strip" if "strip" in type(self).__name__.lower() else "bus"}state',
|
||||
)[self.index],
|
||||
"little",
|
||||
'little',
|
||||
)
|
||||
& getattr(self._modes, f'_{param.replace(".", "_").lower()}')
|
||||
& getattr(self._modes, f'_{param.lower()}')
|
||||
== 0
|
||||
)
|
||||
|
||||
@@ -30,15 +32,15 @@ def channel_bool_prop(param):
|
||||
def channel_label_prop():
|
||||
"""meta function for channel label parameters"""
|
||||
|
||||
@partial(cache_string, param="label")
|
||||
@partial(cache_string, param='label')
|
||||
def fget(self) -> str:
|
||||
return getattr(
|
||||
self.public_packet,
|
||||
f"{'strip' if 'strip' in type(self).__name__.lower() else 'bus'}labels",
|
||||
self.public_packets[NBS.zero],
|
||||
f'{"strip" if "strip" in type(self).__name__.lower() else "bus"}labels',
|
||||
)[self.index]
|
||||
|
||||
def fset(self, val: str):
|
||||
self.setter("label", str(val))
|
||||
self.setter('label', str(val))
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
@@ -48,9 +50,13 @@ def strip_output_prop(param):
|
||||
|
||||
@partial(cache_bool, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
return (
|
||||
not int.from_bytes(self.public_packet.stripstate[self.index], "little")
|
||||
& getattr(self._modes, f"_bus{param.lower()}")
|
||||
not int.from_bytes(
|
||||
self.public_packets[NBS.zero].stripstate[self.index], 'little'
|
||||
)
|
||||
& getattr(self._modes, f'_bus{param.lower()}')
|
||||
== 0
|
||||
)
|
||||
|
||||
@@ -65,26 +71,18 @@ def bus_mode_prop(param):
|
||||
|
||||
@partial(cache_bool, param=param)
|
||||
def fget(self):
|
||||
modelist = {
|
||||
"amix": (1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1),
|
||||
"repeat": (0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2),
|
||||
"bmix": (1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3),
|
||||
"composite": (0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0),
|
||||
"tvmix": (1, 0, 1, 4, 5, 4, 5, 0, 1, 0, 1),
|
||||
"upmix21": (0, 2, 2, 4, 4, 6, 6, 0, 0, 2, 2),
|
||||
"upmix41": (1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3),
|
||||
"upmix61": (0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8),
|
||||
"centeronly": (1, 0, 1, 0, 1, 0, 1, 8, 9, 8, 9),
|
||||
"lfeonly": (0, 2, 2, 0, 0, 2, 2, 8, 8, 10, 10),
|
||||
"rearonly": (1, 2, 3, 0, 1, 2, 3, 8, 9, 10, 11),
|
||||
}
|
||||
vals = (
|
||||
int.from_bytes(self.public_packet.busstate[self.index], "little") & val
|
||||
for val in self._modes.modevals
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
return [
|
||||
(
|
||||
int.from_bytes(
|
||||
self.public_packets[NBS.zero].busstate[self.index], 'little'
|
||||
)
|
||||
if param == "normal":
|
||||
return not any(vals)
|
||||
return tuple(round(val / 16) for val in vals) == modelist[param]
|
||||
& val
|
||||
)
|
||||
>> 4
|
||||
for val in self._modes.modevals
|
||||
] == self.modestates[param]
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, 1 if val else 0)
|
||||
@@ -92,10 +90,68 @@ 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)
|
||||
|
||||
return fdo
|
||||
|
||||
|
||||
def xy_prop(param):
|
||||
"""meta function for XY pad parameters"""
|
||||
|
||||
@partial(cache_float, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
|
||||
positions = self.public_packets[NBS.one].strips[self.index].positions
|
||||
match param:
|
||||
case 'pan_x':
|
||||
return positions.pan_x
|
||||
case 'pan_y':
|
||||
return positions.pan_y
|
||||
case 'color_x':
|
||||
return positions.color_x
|
||||
case 'color_y':
|
||||
return positions.color_y
|
||||
case 'fx1':
|
||||
return positions.fx1
|
||||
case 'fx2':
|
||||
return positions.fx2
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def send_prop(param):
|
||||
"""meta function for send parameters"""
|
||||
|
||||
@partial(cache_float, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
|
||||
sends = self.public_packets[NBS.one].strips[self.index].sends
|
||||
match param:
|
||||
case 'reverb':
|
||||
return sends.reverb
|
||||
case 'delay':
|
||||
return sends.delay
|
||||
case 'fx1':
|
||||
return sends.fx1
|
||||
case 'fx2':
|
||||
return sends.fx2
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
@@ -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,22 +1,41 @@
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
from typing import NamedTuple
|
||||
|
||||
from .enums import NBS
|
||||
from .kinds import KindMapClass
|
||||
from .util import comp
|
||||
|
||||
VBAN_PROTOCOL_TXT = 0x40
|
||||
VBAN_PROTOCOL_SERVICE = 0x60
|
||||
|
||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||
VBAN_SERVICE_RTPACKET = 33
|
||||
VBAN_SERVICE_MASK = 0xE0
|
||||
|
||||
MAX_PACKET_SIZE = 1436
|
||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||
VMPARAMSTRIP_SIZE = 174
|
||||
|
||||
|
||||
@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"""
|
||||
|
||||
nbs: NBS
|
||||
_kind: KindMapClass
|
||||
_voicemeeterType: bytes
|
||||
_reserved: bytes
|
||||
_buffersize: bytes
|
||||
_voicemeeterVersion: bytes
|
||||
_optionBits: bytes
|
||||
_samplerate: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketNBS0(VbanRtPacket):
|
||||
"""Represents the body of a VBAN RT data packet with NBS 0"""
|
||||
|
||||
_inputLeveldB100: bytes
|
||||
_outputLeveldB100: bytes
|
||||
_TransportBit: bytes
|
||||
@@ -34,14 +53,55 @@ class VBAN_VMRT_Packet_Data:
|
||||
_stripLabelUTF8c60: bytes
|
||||
_busLabelUTF8c60: bytes
|
||||
|
||||
def pdirty(self, other):
|
||||
@classmethod
|
||||
def from_bytes(cls, nbs: NBS, kind: KindMapClass, data: bytes):
|
||||
return cls(
|
||||
nbs=nbs,
|
||||
_kind=kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
_inputLeveldB100=data[44:112],
|
||||
_outputLeveldB100=data[112:240],
|
||||
_TransportBit=data[240:244],
|
||||
_stripState=data[244:276],
|
||||
_busState=data[276:308],
|
||||
_stripGaindB100Layer1=data[308:324],
|
||||
_stripGaindB100Layer2=data[324:340],
|
||||
_stripGaindB100Layer3=data[340:356],
|
||||
_stripGaindB100Layer4=data[356:372],
|
||||
_stripGaindB100Layer5=data[372:388],
|
||||
_stripGaindB100Layer6=data[388:404],
|
||||
_stripGaindB100Layer7=data[404:420],
|
||||
_stripGaindB100Layer8=data[420:436],
|
||||
_busGaindB100=data[436:452],
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
|
||||
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,13 +111,22 @@ 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(li) for li in (self._strip_comp, self._bus_comp))
|
||||
|
||||
@property
|
||||
def voicemeetertype(self) -> str:
|
||||
"""returns voicemeeter type as a string"""
|
||||
type_ = ("basic", "banana", "potato")
|
||||
return type_[int.from_bytes(self._voicemeeterType, "little") - 1]
|
||||
type_ = ('basic', 'banana', 'potato')
|
||||
return type_[int.from_bytes(self._voicemeeterType, 'little') - 1]
|
||||
|
||||
@property
|
||||
def voicemeeterversion(self) -> tuple:
|
||||
@@ -65,7 +134,7 @@ class VBAN_VMRT_Packet_Data:
|
||||
return tuple(
|
||||
reversed(
|
||||
tuple(
|
||||
int.from_bytes(self._voicemeeterVersion[i : i + 1], "little")
|
||||
int.from_bytes(self._voicemeeterVersion[i : i + 1], 'little')
|
||||
for i in range(4)
|
||||
)
|
||||
)
|
||||
@@ -74,27 +143,17 @@ class VBAN_VMRT_Packet_Data:
|
||||
@property
|
||||
def samplerate(self) -> int:
|
||||
"""returns samplerate as an int"""
|
||||
return int.from_bytes(self._samplerate, "little")
|
||||
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:
|
||||
@@ -112,74 +171,33 @@ class VBAN_VMRT_Packet_Data:
|
||||
"""
|
||||
|
||||
@property
|
||||
def stripgainlayer1(self) -> tuple:
|
||||
def gainlayers(self) -> tuple:
|
||||
"""returns tuple of all strip gain layers as tuples"""
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
||||
tuple(
|
||||
round(
|
||||
int.from_bytes(
|
||||
getattr(self, f'_stripGaindB100Layer{layer}')[i : i + 2],
|
||||
'little',
|
||||
signed=True,
|
||||
)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
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")
|
||||
for i in range(0, 16, 2)
|
||||
for layer in range(1, 9)
|
||||
)
|
||||
|
||||
@property
|
||||
def busgain(self) -> tuple:
|
||||
"""returns tuple of bus gains"""
|
||||
return tuple(
|
||||
((1 << 16) - 1) - int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
||||
round(
|
||||
int.from_bytes(self._busGaindB100[i : i + 2], 'little', signed=True)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@@ -187,7 +205,7 @@ class VBAN_VMRT_Packet_Data:
|
||||
def striplabels(self) -> tuple:
|
||||
"""returns tuple of strip labels"""
|
||||
return tuple(
|
||||
self._stripLabelUTF8c60[i : i + 60].decode().split("\x00")[0]
|
||||
self._stripLabelUTF8c60[i : i + 60].decode().split('\x00')[0]
|
||||
for i in range(0, 480, 60)
|
||||
)
|
||||
|
||||
@@ -195,94 +213,485 @@ class VBAN_VMRT_Packet_Data:
|
||||
def buslabels(self) -> tuple:
|
||||
"""returns tuple of bus labels"""
|
||||
return tuple(
|
||||
self._busLabelUTF8c60[i : i + 60].decode().split("\x00")[0]
|
||||
self._busLabelUTF8c60[i : i + 60].decode().split('\x00')[0]
|
||||
for i in range(0, 480, 60)
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VBAN_VMRT_Packet_Header:
|
||||
"""Represents a RESPONSE RT PACKET header"""
|
||||
class Audibility(NamedTuple):
|
||||
knob: float
|
||||
comp: float
|
||||
gate: float
|
||||
denoiser: float
|
||||
|
||||
name = "Voicemeeter-RTP"
|
||||
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_RTPACKET).to_bytes(1, "little")
|
||||
format_bit: bytes = (0).to_bytes(1, "little")
|
||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
||||
|
||||
class Positions(NamedTuple):
|
||||
pan_x: float
|
||||
pan_y: float
|
||||
color_x: float
|
||||
color_y: float
|
||||
fx1: float
|
||||
fx2: float
|
||||
|
||||
|
||||
class EqGains(NamedTuple):
|
||||
bass: float
|
||||
mid: float
|
||||
treble: float
|
||||
|
||||
|
||||
class ParametricEQSettings(NamedTuple):
|
||||
on: bool
|
||||
type: int
|
||||
gain: float
|
||||
freq: float
|
||||
q: float
|
||||
|
||||
|
||||
class Sends(NamedTuple):
|
||||
reverb: float
|
||||
delay: float
|
||||
fx1: float
|
||||
fx2: float
|
||||
|
||||
|
||||
class CompressorSettings(NamedTuple):
|
||||
gain_in: float
|
||||
attack_ms: float
|
||||
release_ms: float
|
||||
n_knee: float
|
||||
ratio: float
|
||||
threshold: float
|
||||
c_enabled: bool
|
||||
makeup: bool
|
||||
gain_out: float
|
||||
|
||||
|
||||
class GateSettings(NamedTuple):
|
||||
threshold_in: float
|
||||
damping_max: float
|
||||
bp_sidechain: bool
|
||||
attack_ms: float
|
||||
hold_ms: float
|
||||
release_ms: float
|
||||
|
||||
|
||||
class DenoiserSettings(NamedTuple):
|
||||
threshold: float
|
||||
|
||||
|
||||
class PitchSettings(NamedTuple):
|
||||
enabled: bool
|
||||
dry_wet: float
|
||||
value: float
|
||||
formant_lo: float
|
||||
formant_med: float
|
||||
formant_high: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanVMParamStrip:
|
||||
"""Represents the VBAN_VMPARAMSTRIP_PACKET structure"""
|
||||
|
||||
_mode: bytes
|
||||
_dblevel: bytes
|
||||
_audibility: bytes
|
||||
_pos3D_x: bytes
|
||||
_pos3D_y: bytes
|
||||
_posColor_x: bytes
|
||||
_posColor_y: bytes
|
||||
_EQgain1: bytes
|
||||
_EQgain2: bytes
|
||||
_EQgain3: bytes
|
||||
|
||||
# First channel parametric EQ
|
||||
_PEQ_eqOn: bytes
|
||||
_PEQ_eqtype: bytes
|
||||
_PEQ_eqgain: bytes
|
||||
_PEQ_eqfreq: bytes
|
||||
_PEQ_eqq: bytes
|
||||
|
||||
_audibility_c: bytes
|
||||
_audibility_g: bytes
|
||||
_audibility_d: bytes
|
||||
_posMod_x: bytes
|
||||
_posMod_y: bytes
|
||||
_send_reverb: bytes
|
||||
_send_delay: bytes
|
||||
_send_fx1: bytes
|
||||
_send_fx2: bytes
|
||||
_dblimit: bytes
|
||||
_nKaraoke: bytes
|
||||
|
||||
_COMP_gain_in: bytes
|
||||
_COMP_attack_ms: bytes
|
||||
_COMP_release_ms: bytes
|
||||
_COMP_n_knee: bytes
|
||||
_COMP_comprate: bytes
|
||||
_COMP_threshold: bytes
|
||||
_COMP_c_enabled: bytes
|
||||
_COMP_c_auto: bytes
|
||||
_COMP_gain_out: bytes
|
||||
|
||||
_GATE_dBThreshold_in: bytes
|
||||
_GATE_dBDamping_max: bytes
|
||||
_GATE_BP_Sidechain: bytes
|
||||
_GATE_attack_ms: bytes
|
||||
_GATE_hold_ms: bytes
|
||||
_GATE_release_ms: bytes
|
||||
|
||||
_DenoiserThreshold: bytes
|
||||
_PitchEnabled: bytes
|
||||
_Pitch_DryWet: bytes
|
||||
_Pitch_Value: bytes
|
||||
_Pitch_formant_lo: bytes
|
||||
_Pitch_formant_med: bytes
|
||||
_Pitch_formant_high: bytes
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
return cls(
|
||||
_mode=data[0:4],
|
||||
_dblevel=data[4:8],
|
||||
_audibility=data[8:10],
|
||||
_pos3D_x=data[10:12],
|
||||
_pos3D_y=data[12:14],
|
||||
_posColor_x=data[14:16],
|
||||
_posColor_y=data[16:18],
|
||||
_EQgain1=data[18:20],
|
||||
_EQgain2=data[20:22],
|
||||
_EQgain3=data[22:24],
|
||||
_PEQ_eqOn=data[24:30],
|
||||
_PEQ_eqtype=data[30:36],
|
||||
_PEQ_eqgain=data[36:60],
|
||||
_PEQ_eqfreq=data[60:84],
|
||||
_PEQ_eqq=data[84:108],
|
||||
_audibility_c=data[108:110],
|
||||
_audibility_g=data[110:112],
|
||||
_audibility_d=data[112:114],
|
||||
_posMod_x=data[114:116],
|
||||
_posMod_y=data[116:118],
|
||||
_send_reverb=data[118:120],
|
||||
_send_delay=data[120:122],
|
||||
_send_fx1=data[122:124],
|
||||
_send_fx2=data[124:126],
|
||||
_dblimit=data[126:128],
|
||||
_nKaraoke=data[128:130],
|
||||
_COMP_gain_in=data[130:132],
|
||||
_COMP_attack_ms=data[132:134],
|
||||
_COMP_release_ms=data[134:136],
|
||||
_COMP_n_knee=data[136:138],
|
||||
_COMP_comprate=data[138:140],
|
||||
_COMP_threshold=data[140:142],
|
||||
_COMP_c_enabled=data[142:144],
|
||||
_COMP_c_auto=data[144:146],
|
||||
_COMP_gain_out=data[146:148],
|
||||
_GATE_dBThreshold_in=data[148:150],
|
||||
_GATE_dBDamping_max=data[150:152],
|
||||
_GATE_BP_Sidechain=data[152:154],
|
||||
_GATE_attack_ms=data[154:156],
|
||||
_GATE_hold_ms=data[156:158],
|
||||
_GATE_release_ms=data[158:160],
|
||||
_DenoiserThreshold=data[160:162],
|
||||
_PitchEnabled=data[162:164],
|
||||
_Pitch_DryWet=data[164:166],
|
||||
_Pitch_Value=data[166:168],
|
||||
_Pitch_formant_lo=data[168:170],
|
||||
_Pitch_formant_med=data[170:172],
|
||||
_Pitch_formant_high=data[172:174],
|
||||
)
|
||||
|
||||
@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
|
||||
assert len(header) == HEADER_SIZE - 4, f"Header expected {HEADER_SIZE-4} bytes"
|
||||
return header
|
||||
def mode(self) -> int:
|
||||
return int.from_bytes(self._mode, 'little')
|
||||
|
||||
@property
|
||||
def audibility(self) -> Audibility:
|
||||
return Audibility(
|
||||
round(int.from_bytes(self._audibility, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_c, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_g, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_d, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def positions(self) -> Positions:
|
||||
return Positions(
|
||||
round(int.from_bytes(self._pos3D_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._pos3D_y, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posColor_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posColor_y, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posMod_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posMod_y, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def eqgains(self) -> EqGains:
|
||||
return EqGains(
|
||||
*[
|
||||
round(
|
||||
int.from_bytes(getattr(self, f'_EQgain{i}'), 'little', signed=True)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
for i in range(1, 4)
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def parametric_eq(self) -> tuple[ParametricEQSettings, ...]:
|
||||
return tuple(
|
||||
ParametricEQSettings(
|
||||
on=bool(int.from_bytes(self._PEQ_eqOn[i : i + 1], 'little')),
|
||||
type=int.from_bytes(self._PEQ_eqtype[i : i + 1], 'little'),
|
||||
freq=struct.unpack('<f', self._PEQ_eqfreq[i * 4 : (i + 1) * 4])[0],
|
||||
gain=struct.unpack('<f', self._PEQ_eqgain[i * 4 : (i + 1) * 4])[0],
|
||||
q=struct.unpack('<f', self._PEQ_eqq[i * 4 : (i + 1) * 4])[0],
|
||||
)
|
||||
for i in range(6)
|
||||
)
|
||||
|
||||
@property
|
||||
def sends(self) -> Sends:
|
||||
return Sends(
|
||||
round(int.from_bytes(self._send_reverb, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_delay, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_fx1, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_fx2, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def karaoke(self) -> int:
|
||||
return int.from_bytes(self._nKaraoke, 'little')
|
||||
|
||||
@property
|
||||
def compressor(self) -> CompressorSettings:
|
||||
return CompressorSettings(
|
||||
gain_in=round(
|
||||
int.from_bytes(self._COMP_gain_in, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
attack_ms=round(int.from_bytes(self._COMP_attack_ms, 'little') * 0.1, 2),
|
||||
release_ms=round(int.from_bytes(self._COMP_release_ms, 'little') * 0.1, 2),
|
||||
n_knee=round(int.from_bytes(self._COMP_n_knee, 'little') * 0.01, 2),
|
||||
ratio=round(int.from_bytes(self._COMP_comprate, 'little') * 0.01, 2),
|
||||
threshold=round(
|
||||
int.from_bytes(self._COMP_threshold, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
c_enabled=bool(int.from_bytes(self._COMP_c_enabled, 'little')),
|
||||
makeup=bool(int.from_bytes(self._COMP_c_auto, 'little')),
|
||||
gain_out=round(
|
||||
int.from_bytes(self._COMP_gain_out, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def gate(self) -> GateSettings:
|
||||
return GateSettings(
|
||||
threshold_in=round(
|
||||
int.from_bytes(self._GATE_dBThreshold_in, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
damping_max=round(
|
||||
int.from_bytes(self._GATE_dBDamping_max, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
bp_sidechain=round(
|
||||
int.from_bytes(self._GATE_BP_Sidechain, 'little') * 0.1, 2
|
||||
),
|
||||
attack_ms=round(int.from_bytes(self._GATE_attack_ms, 'little') * 0.1, 2),
|
||||
hold_ms=round(int.from_bytes(self._GATE_hold_ms, 'little') * 0.1, 2),
|
||||
release_ms=round(int.from_bytes(self._GATE_release_ms, 'little') * 0.1, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def denoiser(self) -> DenoiserSettings:
|
||||
return DenoiserSettings(
|
||||
threshold=round(
|
||||
int.from_bytes(self._DenoiserThreshold, 'little', signed=True) * 0.01, 2
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def pitch(self) -> PitchSettings:
|
||||
return PitchSettings(
|
||||
enabled=bool(int.from_bytes(self._PitchEnabled, 'little')),
|
||||
dry_wet=round(
|
||||
int.from_bytes(self._Pitch_DryWet, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
value=round(
|
||||
int.from_bytes(self._Pitch_Value, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_lo=round(
|
||||
int.from_bytes(self._Pitch_formant_lo, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_med=round(
|
||||
int.from_bytes(self._Pitch_formant_med, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_high=round(
|
||||
int.from_bytes(self._Pitch_formant_high, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextRequestHeader:
|
||||
"""Represents a REQUEST RT PACKET header"""
|
||||
class VbanRtPacketNBS1(VbanRtPacket):
|
||||
"""Represents the body of a VBAN RT data packet with NBS 1"""
|
||||
|
||||
strips: tuple[VbanVMParamStrip, ...]
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
nbs: NBS,
|
||||
kind: KindMapClass,
|
||||
data: bytes,
|
||||
):
|
||||
return cls(
|
||||
nbs=nbs,
|
||||
_kind=kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
strips=tuple(
|
||||
VbanVMParamStrip.from_bytes(
|
||||
data[44 + i * VMPARAMSTRIP_SIZE : 44 + (i + 1) * VMPARAMSTRIP_SIZE]
|
||||
)
|
||||
for i in range(16)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubscribeHeader:
|
||||
"""Represents the header of an RT subscription packet"""
|
||||
|
||||
nbs: NBS = NBS.zero
|
||||
name: str = 'Register-RTP'
|
||||
timeout: int = 15
|
||||
|
||||
@property
|
||||
def vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def format_sr(self) -> bytes:
|
||||
return VBAN_PROTOCOL_SERVICE.to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_nbs(self) -> bytes:
|
||||
return (self.nbs.value & 0xFF).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_nbc(self) -> bytes:
|
||||
return VBAN_SERVICE_RTPACKETREGISTER.to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_bit(self) -> bytes:
|
||||
return (self.timeout & 0xFF).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
return self.name.encode('ascii') + bytes(16 - len(self.name))
|
||||
|
||||
@classmethod
|
||||
def to_bytes(cls, nbs: NBS, framecounter: int) -> bytes:
|
||||
header = cls(nbs=nbs)
|
||||
|
||||
data = bytearray()
|
||||
data.extend(header.vban)
|
||||
data.extend(header.format_sr)
|
||||
data.extend(header.format_nbs)
|
||||
data.extend(header.format_nbc)
|
||||
data.extend(header.format_bit)
|
||||
data.extend(header.streamname)
|
||||
data.extend(framecounter.to_bytes(4, 'little'))
|
||||
return bytes(data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketHeader:
|
||||
"""Represents the header of an RT response packet"""
|
||||
|
||||
name: str = 'Voicemeeter-RTP'
|
||||
format_sr: int = VBAN_PROTOCOL_SERVICE
|
||||
format_nbs: int = 0
|
||||
format_nbc: int = VBAN_SERVICE_RTPACKET
|
||||
format_bit: int = 0
|
||||
|
||||
@property
|
||||
def vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
return self.name.encode('ascii') + bytes(16 - len(self.name))
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
if len(data) < HEADER_SIZE:
|
||||
raise ValueError('Data is too short to be a valid VbanRTPPacketHeader')
|
||||
|
||||
name = data[8:24].rstrip(b'\x00').decode('utf-8')
|
||||
return cls(
|
||||
name=name,
|
||||
format_sr=data[4] & VBAN_SERVICE_MASK,
|
||||
format_nbs=data[5],
|
||||
format_nbc=data[6],
|
||||
format_bit=data[7],
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestHeader:
|
||||
"""Represents the header of an RT request packet"""
|
||||
|
||||
name: str
|
||||
bps_index: int
|
||||
channel: int
|
||||
vban: bytes = "VBAN".encode()
|
||||
nbs: bytes = (0).to_bytes(1, "little")
|
||||
bit: bytes = (0x10).to_bytes(1, "little")
|
||||
framecounter: bytes = (0).to_bytes(4, "little")
|
||||
framecounter: int = 0
|
||||
|
||||
@property
|
||||
def sr(self):
|
||||
return (0x40 + self.bps_index).to_bytes(1, "little")
|
||||
def vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def nbc(self):
|
||||
return (self.channel).to_bytes(1, "little")
|
||||
def sr(self) -> bytes:
|
||||
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def streamname(self):
|
||||
def nbs(self) -> bytes:
|
||||
return (0).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def nbc(self) -> bytes:
|
||||
return (self.channel).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def bit(self) -> bytes:
|
||||
return (0x10).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
return self.name.encode() + bytes(16 - len(self.name))
|
||||
|
||||
@property
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.sr
|
||||
header += self.nbs
|
||||
header += self.nbc
|
||||
header += self.bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
||||
return header
|
||||
@classmethod
|
||||
def to_bytes(
|
||||
cls, name: str, bps_index: int, channel: int, framecounter: int
|
||||
) -> bytes:
|
||||
header = cls(
|
||||
name=name, bps_index=bps_index, channel=channel, framecounter=framecounter
|
||||
)
|
||||
|
||||
|
||||
@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"
|
||||
return header
|
||||
data = bytearray()
|
||||
data.extend(header.vban)
|
||||
data.extend(header.sr)
|
||||
data.extend(header.nbs)
|
||||
data.extend(header.nbc)
|
||||
data.extend(header.bit)
|
||||
data.extend(header.streamname)
|
||||
data.extend(header.framecounter.to_bytes(4, 'little'))
|
||||
return bytes(data)
|
||||
|
||||
@@ -1,9 +1,17 @@
|
||||
from abc import abstractmethod
|
||||
import abc
|
||||
import time
|
||||
from typing import Union
|
||||
|
||||
from . import kinds
|
||||
from .enums import NBS
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
from .meta import channel_bool_prop, channel_label_prop, strip_output_prop
|
||||
from .meta import (
|
||||
channel_bool_prop,
|
||||
channel_label_prop,
|
||||
send_prop,
|
||||
strip_output_prop,
|
||||
xy_prop,
|
||||
)
|
||||
|
||||
|
||||
class Strip(IRemote):
|
||||
@@ -13,13 +21,13 @@ class Strip(IRemote):
|
||||
Defines concrete implementation for strip
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
@abc.abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}]"
|
||||
return f'strip[{self.index}]'
|
||||
|
||||
@property
|
||||
def limit(self) -> int:
|
||||
@@ -27,64 +35,491 @@ class Strip(IRemote):
|
||||
|
||||
@limit.setter
|
||||
def limit(self, val: int):
|
||||
self.setter("limit", val)
|
||||
self.setter('limit', val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
val = self.getter("gain")
|
||||
val = self.getter('gain')
|
||||
if val is None:
|
||||
val = self.gainlayer[0].gain
|
||||
val = max(layer.gain for layer in self.gainlayer)
|
||||
return round(val, 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
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, is_phys):
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
f'PhysicalStrip{remote.kind}',
|
||||
(cls, EFFECTS_cls),
|
||||
{
|
||||
'comp': StripComp(remote, index),
|
||||
'gate': StripGate(remote, index),
|
||||
'denoiser': StripDenoiser(remote, index),
|
||||
'eq': StripEQ.make(remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
@property
|
||||
def comp(self) -> float:
|
||||
return
|
||||
def audibility(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.knob
|
||||
|
||||
@comp.setter
|
||||
def comp(self, val: float):
|
||||
self.setter("Comp", val)
|
||||
@audibility.setter
|
||||
def audibility(self, val: float):
|
||||
self.setter('audibility', val)
|
||||
|
||||
|
||||
class StripComp(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'strip[{self.index}].comp'
|
||||
|
||||
@property
|
||||
def gate(self) -> float:
|
||||
return
|
||||
def knob(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.comp
|
||||
|
||||
@gate.setter
|
||||
def gate(self, val: float):
|
||||
self.setter("gate", val)
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter('', val)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return
|
||||
def gainin(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.gain_in
|
||||
|
||||
@gainin.setter
|
||||
def gainin(self, val: float):
|
||||
self.setter('GainIn', val)
|
||||
|
||||
@property
|
||||
def sr(self):
|
||||
def ratio(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.ratio
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, val: float):
|
||||
self.setter('Ratio', val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.threshold
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter('Threshold', val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.attack_ms
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter('Attack', val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.release_ms
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
self.setter('Release', val)
|
||||
|
||||
@property
|
||||
def knee(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.n_knee
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: float):
|
||||
self.setter('Knee', val)
|
||||
|
||||
@property
|
||||
def gainout(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.gain_out
|
||||
|
||||
@gainout.setter
|
||||
def gainout(self, val: float):
|
||||
self.setter('GainOut', val)
|
||||
|
||||
@property
|
||||
def makeup(self) -> bool:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return False
|
||||
return bool(self.public_packets[NBS.one].strips[self.index].compressor.makeup)
|
||||
|
||||
@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:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.gate
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter('', val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.threshold_in
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter('Threshold', val)
|
||||
|
||||
@property
|
||||
def damping(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.damping_max
|
||||
|
||||
@damping.setter
|
||||
def damping(self, val: float):
|
||||
self.setter('Damping', val)
|
||||
|
||||
@property
|
||||
def bpsidechain(self) -> int:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.bp_sidechain
|
||||
|
||||
@bpsidechain.setter
|
||||
def bpsidechain(self, val: int):
|
||||
self.setter('BPSidechain', val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.attack_ms
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter('Attack', val)
|
||||
|
||||
@property
|
||||
def hold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.hold_ms
|
||||
|
||||
@hold.setter
|
||||
def hold(self, val: float):
|
||||
self.setter('Hold', val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.release_ms
|
||||
|
||||
@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:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.denoiser
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter('', val)
|
||||
|
||||
|
||||
class StripEQ(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i):
|
||||
"""
|
||||
Factory method for Strip EQ.
|
||||
|
||||
Returns a StripEQ class.
|
||||
"""
|
||||
STRIPEQ_cls = type(
|
||||
'StripEQ',
|
||||
(cls,),
|
||||
{
|
||||
'channel': tuple(
|
||||
StripEQCh.make(remote, i, j)
|
||||
for j in range(remote.kind.strip_channels)
|
||||
)
|
||||
},
|
||||
)
|
||||
return STRIPEQ_cls(remote, i)
|
||||
|
||||
@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 StripEQCh(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i, j):
|
||||
"""
|
||||
Factory method for Strip EQ channel.
|
||||
|
||||
Returns a StripEQCh class.
|
||||
"""
|
||||
StripEQCh_cls = type(
|
||||
'StripEQCh',
|
||||
(cls,),
|
||||
{
|
||||
'cell': tuple(
|
||||
StripEQChCell(remote, i, j, k) for k in range(remote.kind.cells)
|
||||
)
|
||||
},
|
||||
)
|
||||
return StripEQCh_cls(remote, i, j)
|
||||
|
||||
def __init__(self, remote, i, j):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Strip[{self.index}].eq.channel[{self.channel_index}]'
|
||||
|
||||
|
||||
class StripEQChCell(IRemote):
|
||||
def __init__(self, remote, i, j, k):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
self.cell_index = k
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Strip[{self.index}].eq.channel[{self.channel_index}].cell[{self.cell_index}]'
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return False
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.on
|
||||
)
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter('on', 1 if val else 0)
|
||||
|
||||
@property
|
||||
def type(self) -> int:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.type
|
||||
)
|
||||
|
||||
@type.setter
|
||||
def type(self, val: int):
|
||||
self.setter('type', val)
|
||||
|
||||
@property
|
||||
def f(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.freq
|
||||
)
|
||||
|
||||
@f.setter
|
||||
def f(self, val: float):
|
||||
self.setter('f', val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.gain
|
||||
)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter('gain', val)
|
||||
|
||||
@property
|
||||
def q(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.q
|
||||
)
|
||||
|
||||
@q.setter
|
||||
def q(self, val: float):
|
||||
self.setter('q', val)
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
@classmethod
|
||||
def make(cls, remote, i, is_phys):
|
||||
"""
|
||||
Factory method for VirtualStrip.
|
||||
|
||||
mc = channel_bool_prop("mc")
|
||||
Returns a VirtualStrip class.
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
'VirtualStrip',
|
||||
(cls, EFFECTS_cls),
|
||||
{},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
mc = channel_bool_prop('mc')
|
||||
|
||||
mono = mc
|
||||
|
||||
@property
|
||||
def k(self) -> int:
|
||||
return
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return self.public_packets[NBS.one].strips[self.index].karaoke
|
||||
|
||||
@k.setter
|
||||
def k(self, val: int):
|
||||
self.setter("karaoke", val)
|
||||
self.setter('karaoke', val)
|
||||
|
||||
@property
|
||||
def bass(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.bass
|
||||
|
||||
@bass.setter
|
||||
def bass(self, val: float):
|
||||
self.setter('EQGain1', val)
|
||||
|
||||
@property
|
||||
def mid(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.mid
|
||||
|
||||
@mid.setter
|
||||
def mid(self, val: float):
|
||||
self.setter('EQGain2', val)
|
||||
|
||||
med = mid
|
||||
|
||||
@property
|
||||
def treble(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.treble
|
||||
|
||||
@treble.setter
|
||||
def treble(self, val: float):
|
||||
self.setter('EQGain3', val)
|
||||
|
||||
high = treble
|
||||
|
||||
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):
|
||||
@@ -103,14 +538,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_packets[NBS.zero])[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,36 +592,28 @@ 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
|
||||
val = self.getter(f'GainLayer[{self._i}]')
|
||||
if val:
|
||||
return round(val, 2)
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
|
||||
val = self.getter(f"GainLayer[{self._i}]")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return self.public_packets[NBS.zero].gainlayers[self._i][self.index]
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter(f"GainLayer[{self._i}]", val)
|
||||
self.setter(f'GainLayer[{self._i}]', val)
|
||||
|
||||
|
||||
def _make_gainlayer_mixin(remote, index):
|
||||
"""Creates a GainLayer mixin"""
|
||||
return type(
|
||||
f"GainlayerMixin",
|
||||
'GainlayerMixin',
|
||||
(),
|
||||
{
|
||||
"gainlayer": tuple(
|
||||
'gainlayer': tuple(
|
||||
GainLayer(remote, index, i) for i in range(remote.kind.num_bus)
|
||||
)
|
||||
},
|
||||
@@ -182,24 +623,78 @@ def _make_gainlayer_mixin(remote, index):
|
||||
def _make_channelout_mixin(kind):
|
||||
"""Creates a channel out property mixin"""
|
||||
return type(
|
||||
f"ChannelOutMixin{kind}",
|
||||
f'ChannelOutMixin{kind}',
|
||||
(),
|
||||
{
|
||||
**{
|
||||
f"A{i}": strip_output_prop(f"A{i}") for i in range(1, kind.phys_out + 1)
|
||||
f'A{i}': strip_output_prop(f'A{i}') for i in range(1, kind.phys_out + 1)
|
||||
},
|
||||
**{
|
||||
f"B{i}": strip_output_prop(f"B{i}") for i in range(1, kind.virt_out + 1)
|
||||
f'B{i}': strip_output_prop(f'B{i}') for i in range(1, kind.virt_out + 1)
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
_make_channelout_mixins = {
|
||||
kind.name: _make_channelout_mixin(kind) for kind in kinds_all
|
||||
kind.name: _make_channelout_mixin(kind) for kind in kinds.all
|
||||
}
|
||||
|
||||
|
||||
def _make_effects_mixin(kind, is_phys):
|
||||
"""creates an effects mixin for a kind"""
|
||||
|
||||
def _make_xy_cls():
|
||||
pan = {param: xy_prop(param) for param in ['pan_x', 'pan_y']}
|
||||
color = {param: xy_prop(param) for param in ['color_x', 'color_y']}
|
||||
fx = {param: xy_prop(param) for param in ['fx_x', 'fx_y']}
|
||||
if is_phys:
|
||||
return type(
|
||||
'XYPhys',
|
||||
(),
|
||||
{
|
||||
**pan,
|
||||
**color,
|
||||
**fx,
|
||||
},
|
||||
)
|
||||
return type(
|
||||
'XYVirt',
|
||||
(),
|
||||
{**pan},
|
||||
)
|
||||
|
||||
def _make_sends_cls():
|
||||
if is_phys:
|
||||
return type(
|
||||
'FX',
|
||||
(),
|
||||
{
|
||||
**{
|
||||
param: send_prop(param)
|
||||
for param in ['reverb', 'delay', 'fx1', 'fx2']
|
||||
},
|
||||
# **{
|
||||
# f'post{param}': bool_prop(f'post{param}')
|
||||
# for param in ['reverb', 'delay', 'fx1', 'fx2']
|
||||
# },
|
||||
},
|
||||
)
|
||||
return type('FX', (), {})
|
||||
|
||||
if kind.name == 'basic':
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == 'banana':
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == 'potato':
|
||||
steps = (_make_xy_cls, _make_sends_cls)
|
||||
return type(f'Effects{kind}', tuple(step() for step in steps), {})
|
||||
|
||||
|
||||
def _make_effects_mixins(is_phys):
|
||||
return {kind.name: _make_effects_mixin(kind, is_phys) for kind in kinds.all}
|
||||
|
||||
|
||||
def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip]:
|
||||
"""
|
||||
Factory method for strips
|
||||
@@ -208,17 +703,21 @@ 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, is_phys_strip)
|
||||
if is_phys_strip
|
||||
else VirtualStrip.make(remote, i, is_phys_strip)
|
||||
)
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||
|
||||
return type(
|
||||
f"{STRIP_cls.__name__}{remote.kind}",
|
||||
f'{STRIP_cls.__name__}{remote.kind}',
|
||||
(STRIP_cls, CHANNELOUTMIXIN_cls, GAINLAYERMIXIN_cls),
|
||||
{
|
||||
"levels": StripLevel(remote, i),
|
||||
**{param: channel_bool_prop(param) for param in ["mono", "solo", "mute"]},
|
||||
"label": channel_label_prop(),
|
||||
'levels': StripLevel(remote, i),
|
||||
**{param: channel_bool_prop(param) for param in ['mono', 'solo', 'mute']},
|
||||
'label': channel_label_prop(),
|
||||
},
|
||||
)(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()
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from enum import IntEnum
|
||||
from typing import Iterator
|
||||
|
||||
|
||||
@@ -7,9 +6,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 +20,24 @@ 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
|
||||
|
||||
|
||||
def cache_float(func, param):
|
||||
"""Check cache for a float prop"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
self, *rem = args
|
||||
if self._cmd(param) in self._remote.cache:
|
||||
return round(self._remote.cache.pop(self._cmd(param)), 2)
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -40,15 +55,15 @@ def script(func):
|
||||
def wrapper(*args):
|
||||
remote, script = args
|
||||
if isinstance(script, dict):
|
||||
params = ""
|
||||
params = ''
|
||||
for key, val in script.items():
|
||||
obj, m2, *rem = key.split("-")
|
||||
obj, m2, *rem = key.split('-')
|
||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
||||
params += ";".join(
|
||||
f"{obj}{f'.{m2}stream' if not m2.isnumeric() else ''}[{index}].{k}={int(v) if isinstance(v, bool) else v}"
|
||||
params += ';'.join(
|
||||
f'{obj}{f".{m2}stream" if not m2.isnumeric() else ""}[{index}].{k}={int(v) if isinstance(v, bool) else v}'
|
||||
for k, v in val.items()
|
||||
)
|
||||
params += ";"
|
||||
params += ';'
|
||||
script = params
|
||||
return func(remote, script)
|
||||
|
||||
@@ -61,10 +76,31 @@ 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
|
||||
|
||||
|
||||
Socket = IntEnum("Socket", "register request response", start=0)
|
||||
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]
|
||||
|
||||
|
||||
def bump_framecounter(framecounter: int) -> int:
|
||||
"""Increment framecounter with rollover at 0xFFFFFFFF."""
|
||||
if framecounter > 0xFFFFFFFF:
|
||||
return 0
|
||||
else:
|
||||
return framecounter + 1
|
||||
|
||||
234
vban_cmd/vban.py
Normal file
234
vban_cmd/vban.py
Normal file
@@ -0,0 +1,234 @@
|
||||
import abc
|
||||
|
||||
from . import kinds
|
||||
from .iremote import IRemote
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
"""
|
||||
Implements the common interface
|
||||
|
||||
Defines concrete implementation for vban stream
|
||||
"""
|
||||
|
||||
@abc.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)
|
||||
248
vban_cmd/vbancmd.py
Normal file
248
vban_cmd/vbancmd.py
Normal file
@@ -0,0 +1,248 @@
|
||||
import abc
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Iterable, Union
|
||||
|
||||
from .enums import NBS
|
||||
from .error import VBANCMDError
|
||||
from .event import Event
|
||||
from .packet import RequestHeader
|
||||
from .subject import Subject
|
||||
from .util import bump_framecounter, deep_merge, script
|
||||
from .worker import Producer, Subscriber, Updater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VbanCmd(abc.ABC):
|
||||
"""Abstract Base Class for Voicemeeter VBAN Command Interfaces"""
|
||||
|
||||
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._framecounter = 0
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.subject = self.observer = Subject()
|
||||
self.cache = {}
|
||||
self._pdirty = False
|
||||
self._ldirty = False
|
||||
self._script = str()
|
||||
self.stop_event = None
|
||||
self.producer = None
|
||||
|
||||
@abc.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 # type: ignore[import]
|
||||
|
||||
def get_filepath():
|
||||
for pn in (
|
||||
Path.cwd() / 'vban.toml',
|
||||
Path.cwd() / 'configs' / 'vban.toml',
|
||||
Path.home() / '.config' / 'vban-cmd' / 'vban.toml',
|
||||
Path.home() / 'Documents' / 'Voicemeeter' / 'configs' / 'vban.toml',
|
||||
):
|
||||
if pn.exists():
|
||||
return pn
|
||||
|
||||
if not (filepath := get_filepath()):
|
||||
raise VBANCMDError('no ip provided and no vban.toml located.')
|
||||
try:
|
||||
with open(filepath, 'rb') as f:
|
||||
return tomllib.load(f)['connection']
|
||||
except tomllib.TomlDecodeError as e:
|
||||
raise VBANCMDError(f'Error decoding {filepath}: {e}') from e
|
||||
|
||||
def __enter__(self):
|
||||
self.login()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
||||
self.logout()
|
||||
|
||||
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 logout(self) -> None:
|
||||
if not self.stopped():
|
||||
self.logger.debug('events thread shutdown started')
|
||||
self.stop_event.set()
|
||||
if self.producer is not None:
|
||||
for t in (self.producer, self.subscriber):
|
||||
t.join()
|
||||
self.sock.close()
|
||||
self.logger.info(f'{type(self).__name__}: Successfully logged out of {self}')
|
||||
|
||||
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."""
|
||||
req_packet = RequestHeader.to_bytes(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
framecounter=self._framecounter,
|
||||
)
|
||||
self.sock.sendto(
|
||||
req_packet + f'{cmd}={val};'.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
self.cache[cmd] = val
|
||||
|
||||
@script
|
||||
def sendtext(self, script):
|
||||
"""Sends a multiple parameter string over a network."""
|
||||
req_packet = RequestHeader.to_bytes(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
framecounter=self._framecounter,
|
||||
)
|
||||
self.sock.sendto(
|
||||
req_packet + script.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
self.logger.debug(f'sendtext: {script}')
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation."""
|
||||
return self.public_packets[NBS.zero].voicemeetertype
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
return '{0}.{1}.{2}.{3}'.format(
|
||||
*self.public_packets[NBS.zero].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_packets(self):
|
||||
return self._public_packets
|
||||
|
||||
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!")
|
||||
@@ -1,40 +1,148 @@
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from .enums import NBS
|
||||
from .error import VBANCMDConnectionError
|
||||
from .packet import (
|
||||
HEADER_SIZE,
|
||||
RegisterRTHeader,
|
||||
VBAN_VMRT_Packet_Data,
|
||||
VBAN_VMRT_Packet_Header,
|
||||
VBAN_PROTOCOL_SERVICE,
|
||||
VBAN_SERVICE_RTPACKET,
|
||||
SubscribeHeader,
|
||||
VbanRtPacket,
|
||||
VbanRtPacketHeader,
|
||||
VbanRtPacketNBS0,
|
||||
VbanRtPacketNBS1,
|
||||
)
|
||||
from .util import Socket
|
||||
from .util import bump_framecounter
|
||||
|
||||
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._framecounter = 0
|
||||
|
||||
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),
|
||||
for nbs in NBS:
|
||||
sub_packet = SubscribeHeader().to_bytes(nbs, self._framecounter)
|
||||
self._remote.sock.sendto(
|
||||
sub_packet, (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._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
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 Producer(threading.Thread):
|
||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||
|
||||
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._remote.sock.settimeout(self._remote.timeout)
|
||||
self._remote._public_packets = [None] * (max(NBS) + 1)
|
||||
_pp = self._get_rt()
|
||||
self._remote._public_packets[_pp.nbs] = _pp
|
||||
(
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
) = self._remote._get_levels(self._remote.public_packets[NBS.zero])
|
||||
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
while True:
|
||||
if resp := self._fetch_rt_packet():
|
||||
return resp
|
||||
|
||||
def _fetch_rt_packet(self) -> VbanRtPacket | None:
|
||||
try:
|
||||
data, _ = self._remote.sock.recvfrom(2048)
|
||||
if len(data) < HEADER_SIZE:
|
||||
return
|
||||
|
||||
response_header = VbanRtPacketHeader.from_bytes(data[:HEADER_SIZE])
|
||||
if (
|
||||
response_header.format_sr != VBAN_PROTOCOL_SERVICE
|
||||
or response_header.format_nbc != VBAN_SERVICE_RTPACKET
|
||||
):
|
||||
return
|
||||
|
||||
match response_header.format_nbs:
|
||||
case NBS.zero:
|
||||
return VbanRtPacketNBS0.from_bytes(
|
||||
nbs=NBS.zero, kind=self._remote.kind, data=data
|
||||
)
|
||||
|
||||
case NBS.one:
|
||||
return VbanRtPacketNBS1.from_bytes(
|
||||
nbs=NBS.one, kind=self._remote.kind, data=data
|
||||
)
|
||||
return None
|
||||
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 stopped(self):
|
||||
return self.stop_event.is_set()
|
||||
|
||||
def run(self):
|
||||
while not self.stopped():
|
||||
pdirty = ldirty = False
|
||||
_pp = self._get_rt()
|
||||
match _pp.nbs:
|
||||
case NBS.zero:
|
||||
ldirty = _pp.ldirty(
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
)
|
||||
pdirty = _pp.pdirty(self._remote.public_packets[NBS.zero])
|
||||
case NBS.one:
|
||||
pdirty = True
|
||||
|
||||
if pdirty or ldirty:
|
||||
self._remote._public_packets[_pp.nbs] = _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):
|
||||
@@ -44,80 +152,34 @@ class Updater(threading.Thread):
|
||||
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):
|
||||
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_packets[NBS.zero]._strip_comp,
|
||||
self._remote._public_packets[NBS.zero]._bus_comp,
|
||||
)
|
||||
self.expected_packet = VBAN_VMRT_Packet_Header()
|
||||
self._rem._public_packet = self._get_rt()
|
||||
|
||||
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
|
||||
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(
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
_inputLeveldB100=data[44:112],
|
||||
_outputLeveldB100=data[112:240],
|
||||
_TransportBit=data[240:244],
|
||||
_stripState=data[244:276],
|
||||
_busState=data[276:308],
|
||||
_stripGaindB100Layer1=data[308:324],
|
||||
_stripGaindB100Layer2=data[324:340],
|
||||
_stripGaindB100Layer3=data[340:356],
|
||||
_stripGaindB100Layer4=data[356:372],
|
||||
_stripGaindB100Layer5=data[372:388],
|
||||
_stripGaindB100Layer6=data[388:404],
|
||||
_stripGaindB100Layer7=data[404:420],
|
||||
_stripGaindB100Layer8=data[420:436],
|
||||
_busGaindB100=data[436:452],
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
|
||||
def _get_rt(self) -> VBAN_VMRT_Packet_Data:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
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()
|
||||
_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)
|
||||
|
||||
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)
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
) = (
|
||||
self._remote._public_packets[NBS.zero].inputlevels,
|
||||
self._remote._public_packets[NBS.zero].outputlevels,
|
||||
)
|
||||
self._remote.subject.notify(event)
|
||||
self.logger.debug(f'terminating {self.name} thread')
|
||||
|
||||
Reference in New Issue
Block a user