mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-07 16:13:30 +00:00
Compare commits
79 Commits
add-event-
...
v2.8.1
| Author | SHA1 | Date | |
|---|---|---|---|
| 23b99cb66b | |||
| 2fd7b8ad8b | |||
| c851cb5abe | |||
| dc681f50d0 | |||
| a0ec00652b | |||
| 69263c22f2 | |||
| ad2cfeaae6 | |||
| 1123fe6432 | |||
| 3c3e415d7e | |||
| 8cfeb45fcb | |||
| 10b38b3fcc | |||
| ff5ac193c8 | |||
| 2f3cd0e07f | |||
| d689b3a301 | |||
| 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 |
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'
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -151,8 +151,8 @@ cython_debug/
|
|||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||||
#.idea/
|
#.idea/
|
||||||
|
|
||||||
# quick test
|
# test files
|
||||||
quick.py
|
test-*.py
|
||||||
|
|
||||||
#config
|
#config
|
||||||
config.toml
|
config.toml
|
||||||
|
|||||||
46
CHANGELOG.md
46
CHANGELOG.md
@@ -11,6 +11,52 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
|
## [2.7.0] - 2026-03-01
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- new kind `matrix` has been added, it does two things:
|
||||||
|
- scales the interface according to `potato` kind, in practice this has no affect but it's required by the builder classes.
|
||||||
|
- disables the rt listener threads since we aren't expecting to receive any from a Matrix VBAN server.
|
||||||
|
- however, matrix responses may still be received with the {VbanCmd}.sendtext() method.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `outbound` kwarg has been renamed to `disable_rt_listeners`. Since it's job is to disable the listener threads for incoming RT packets this new name is more descriptive.
|
||||||
|
- dataclasses representing packet headers and packets with ident:0 and ident:1 have been moved into an internal packet module.
|
||||||
|
|
||||||
|
### Removed
|
||||||
|
|
||||||
|
- {VbanCmd}.sendtext() @script decorator removed. It's purpose was to attempt to convert a dictionary to a script but it was poorly implemented and there exists the {VbanCmd}.apply() method already.
|
||||||
|
|
||||||
|
## [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
|
## [2.3.2] - 2023-07-12
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
177
README.md
177
README.md
@@ -1,26 +1,26 @@
|
|||||||
[](https://badge.fury.io/py/vban-cmd)
|
[](https://badge.fury.io/py/vban-cmd)
|
||||||
[](https://github.com/onyx-and-iris/vban-cmd-python/blob/dev/LICENSE)
|
[](https://github.com/onyx-and-iris/vban-cmd-python/blob/dev/LICENSE)
|
||||||
[](https://github.com/psf/black)
|
[](https://python-poetry.org/)
|
||||||
[](https://pycqa.github.io/isort/)
|
[](https://github.com/astral-sh/ruff)
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
# VBAN CMD
|
# VBAN CMD
|
||||||
|
|
||||||
This python interface allows you to transmit Voicemeeter parameters over a network.
|
This python interface allows you to send Voicemeeter/Matrix commands 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)
|
It offers the same public API as [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python).
|
||||||
|
|
||||||
There is no support for audio transfer in this package, only parameters.
|
Only the VBAN SERVICE/TEXT subprotocols are supported, there is no support for AUDIO or MIDI in this package.
|
||||||
|
|
||||||
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||||
|
|
||||||
## Tested against
|
## Tested against
|
||||||
|
|
||||||
- Basic 1.0.8.8
|
- Basic 1.1.2.2
|
||||||
- Banana 2.0.6.8
|
- Banana 2.1.2.2
|
||||||
- Potato 3.0.2.8
|
- Potato 3.1.2.2
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -29,7 +29,9 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
|||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
`pip install vban-cmd`
|
```console
|
||||||
|
pip install vban-cmd
|
||||||
|
```
|
||||||
|
|
||||||
## `Use`
|
## `Use`
|
||||||
|
|
||||||
@@ -63,27 +65,27 @@ class ManyThings:
|
|||||||
self.vban = vban
|
self.vban = vban
|
||||||
|
|
||||||
def things(self):
|
def things(self):
|
||||||
self.vban.strip[0].label = "podmic"
|
self.vban.strip[0].label = 'podmic'
|
||||||
self.vban.strip[0].mute = True
|
self.vban.strip[0].mute = True
|
||||||
print(
|
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):
|
def other_things(self):
|
||||||
self.vban.bus[3].gain = -6.3
|
self.vban.bus[3].gain = -6.3
|
||||||
self.vban.bus[4].eq.on = True
|
self.vban.bus[4].eq = True
|
||||||
info = (
|
info = (
|
||||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
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.on}",
|
f'bus 4 eq has been set to {self.vban.bus[4].eq}',
|
||||||
)
|
)
|
||||||
print("\n".join(info))
|
print('\n'.join(info))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
KIND_ID = "banana"
|
KIND_ID = 'banana'
|
||||||
|
|
||||||
with vban_cmd.api(
|
with vban_cmd.api(
|
||||||
KIND_ID, ip="gamepc.local", port=6980, streamname="Command1"
|
KIND_ID, ip='gamepc.local', port=6980, streamname='Command1'
|
||||||
) as vban:
|
) as vban:
|
||||||
do = ManyThings(vban)
|
do = ManyThings(vban)
|
||||||
do.things()
|
do.things()
|
||||||
@@ -92,13 +94,14 @@ def main():
|
|||||||
# set many parameters at once
|
# set many parameters at once
|
||||||
vban.apply(
|
vban.apply(
|
||||||
{
|
{
|
||||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
'strip-2': {'A1': True, 'B1': True, 'gain': -6.0},
|
||||||
"bus-2": {"mute": True, "eq": {"on": True}},
|
'bus-2': {'mute': True},
|
||||||
|
'vban-in-0': {'on': True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -112,6 +115,8 @@ Pass the kind of Voicemeeter as an argument. KIND_ID may be:
|
|||||||
- `banana`
|
- `banana`
|
||||||
- `potato`
|
- `potato`
|
||||||
|
|
||||||
|
A fourth kind `matrix` has been added, if you pass it as a KIND_ID you are expected to use the [{VbanCmd}.sendtext()](https://github.com/onyx-and-iris/vban-cmd-python?tab=readme-ov-file#vbansendtextscript) method for sending text requests.
|
||||||
|
|
||||||
## `Available commands`
|
## `Available commands`
|
||||||
|
|
||||||
### Strip
|
### Strip
|
||||||
@@ -146,8 +151,8 @@ Set mute state as value for the app matching name.
|
|||||||
example:
|
example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.strip[5].appmute("Spotify", True)
|
vban.strip[5].appmute('Spotify', True)
|
||||||
vban.strip[5].appgain("Spotify", 0.5)
|
vban.strip[5].appgain('Spotify', 0.5)
|
||||||
```
|
```
|
||||||
|
|
||||||
##### Strip.Comp
|
##### Strip.Comp
|
||||||
@@ -170,9 +175,7 @@ example:
|
|||||||
print(vban.strip[4].comp.knob)
|
print(vban.strip[4].comp.knob)
|
||||||
```
|
```
|
||||||
|
|
||||||
Strip Comp properties are defined as write only.
|
Strip Comp `knob` is defined for all versions, all other parameters potato only.
|
||||||
|
|
||||||
`knob` defined for all versions, all other parameters potato only.
|
|
||||||
|
|
||||||
##### Strip.Gate
|
##### Strip.Gate
|
||||||
|
|
||||||
@@ -192,9 +195,7 @@ example:
|
|||||||
vban.strip[2].gate.attack = 300.8
|
vban.strip[2].gate.attack = 300.8
|
||||||
```
|
```
|
||||||
|
|
||||||
Strip Gate properties are defined as write only, potato version only.
|
Strip Gate `knob` is defined for all versions, all other parameters potato only.
|
||||||
|
|
||||||
`knob` defined for all versions, all other parameters potato only.
|
|
||||||
|
|
||||||
##### Strip.Denoiser
|
##### Strip.Denoiser
|
||||||
|
|
||||||
@@ -211,7 +212,32 @@ The following properties are available.
|
|||||||
- `on`: boolean
|
- `on`: boolean
|
||||||
- `ab`: boolean
|
- `ab`: boolean
|
||||||
|
|
||||||
Strip EQ properties are defined as write only, potato version only.
|
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
|
##### Gainlayers
|
||||||
|
|
||||||
@@ -323,6 +349,40 @@ vban.strip[0].fadeto(-10.3, 1000)
|
|||||||
vban.bus[3].fadeby(-5.6, 500)
|
vban.bus[3].fadeby(-5.6, 500)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Recorder
|
||||||
|
|
||||||
|
The following methods are available
|
||||||
|
|
||||||
|
- `play()`
|
||||||
|
- `stop()`
|
||||||
|
- `pause()`
|
||||||
|
- `record()`
|
||||||
|
- `ff()`
|
||||||
|
- `rew()`
|
||||||
|
- `load(filepath)`: raw string
|
||||||
|
- `goto(time_string)`: time string in format `hh:mm:ss`
|
||||||
|
|
||||||
|
The following properties are available
|
||||||
|
|
||||||
|
- `samplerate`: int, (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
|
||||||
|
- `bitresolution`: int, (8, 16, 24, 32)
|
||||||
|
- `channel`: int, from 1 to 8
|
||||||
|
- `kbps`: int, (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
|
||||||
|
- `gain`: float, from -60.0 to 12.0
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
vban.recorder.play()
|
||||||
|
vban.recorder.stop()
|
||||||
|
|
||||||
|
# filepath as raw string
|
||||||
|
vban.recorder.load(r'C:\music\mytune.mp3')
|
||||||
|
|
||||||
|
# set the goto time to 1m 30s
|
||||||
|
vban.recorder.goto('00:01:30')
|
||||||
|
```
|
||||||
|
|
||||||
### Command
|
### Command
|
||||||
|
|
||||||
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:
|
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:
|
||||||
@@ -352,9 +412,10 @@ vban.command.showvbanchat = true
|
|||||||
```python
|
```python
|
||||||
vban.apply(
|
vban.apply(
|
||||||
{
|
{
|
||||||
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
'strip-0': {'A1': True, 'B1': True, 'gain': -6.0},
|
||||||
"bus-1": {"mute": True, "mode": "composite"},
|
'bus-1': {'mute': True, 'mode': 'composite'},
|
||||||
"bus-2": {"eq": {"on": True}},
|
'bus-2': {'eq': {'on': True}},
|
||||||
|
'vban-in-0': {'on': True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -362,8 +423,8 @@ vban.apply(
|
|||||||
Or for each class you may do:
|
Or for each class you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
vban.strip[0].apply({'mute': True, 'gain': 3.2, 'A1': True})
|
||||||
vban.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
vban.vban.outstream[0].apply({'on': True, 'name': 'streamname', 'bit': 24})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config Files
|
## Config Files
|
||||||
@@ -398,8 +459,8 @@ You just need to define a key `extends` in the config TOML, that names the confi
|
|||||||
Three example 'extender' configs are included with the repo. You may load them with:
|
Three example 'extender' configs are included with the repo. You may load them with:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import voicemeeterlib
|
import vban_cmd
|
||||||
with voicemeeterlib.api('banana') as vm:
|
with vban_cmd.api('banana') as vm:
|
||||||
vm.apply_config('extender')
|
vm.apply_config('extender')
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -411,10 +472,11 @@ example:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
"ip": "<ip address>",
|
'ip': '<ip address>',
|
||||||
"streamname": "Command1",
|
'streamname': 'Command1',
|
||||||
"port": 6980,
|
'port': 6980,
|
||||||
}
|
}
|
||||||
with vban_cmd.api('banana', ldirty=True, **opts) as vban:
|
with vban_cmd.api('banana', ldirty=True, **opts) as vban:
|
||||||
...
|
...
|
||||||
@@ -467,7 +529,7 @@ The following methods are available:
|
|||||||
example:
|
example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.event.remove(["pdirty", "ldirty"])
|
vban.event.remove(['pdirty', 'ldirty'])
|
||||||
|
|
||||||
# get a list of currently subscribed
|
# get a list of currently subscribed
|
||||||
print(vban.event.get())
|
print(vban.event.get())
|
||||||
@@ -479,13 +541,16 @@ print(vban.event.get())
|
|||||||
|
|
||||||
You may pass the following optional keyword arguments:
|
You may pass the following optional keyword arguments:
|
||||||
|
|
||||||
- `ip`: str, ip or hostname of remote machine
|
- `ip`: str='localhost', ip or hostname of remote machine
|
||||||
- `streamname`: str, name of the stream to connect to.
|
|
||||||
- `port`: int=6980, vban udp port 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
|
- `pdirty`: boolean=False, parameter updates
|
||||||
- `ldirty`: boolean=False, level updates
|
- `ldirty`: boolean=False, level updates
|
||||||
- `timeout`: int=5, amount of time (seconds) to wait for an incoming RT data packet (parameter states).
|
- `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)
|
- `disable_rt_listeners`: boolean=False, set `True` if you don't wish to receive RT packets.
|
||||||
|
- You can still send Matrix string requests ending with `?` and receive a response.
|
||||||
|
|
||||||
#### `vban.pdirty`
|
#### `vban.pdirty`
|
||||||
|
|
||||||
@@ -500,18 +565,20 @@ True iff a level value has been changed.
|
|||||||
Sends a script block as a string request, for example:
|
Sends a script block as a string request, for example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.sendtext("Strip[0].Mute=1;Bus[0].Mono=1")
|
vban.sendtext('Strip[0].Mute=1;Bus[0].Mono=1')
|
||||||
```
|
```
|
||||||
|
|
||||||
#### `vban.public_packet`
|
You can even use it to send matrix commands:
|
||||||
|
|
||||||
Returns a `VbanRtPacket`. Designed to be used internally by the interface but available for parsing through this read only property object.
|
```python
|
||||||
|
vban.sendtext('Point(ASIO128.IN[1..4],ASIO128.OUT[1]).dBGain = -3.0')
|
||||||
|
|
||||||
States not guaranteed to be current (requires use of dirty parameters to confirm).
|
vban.sendtext('Command.Version = ?')
|
||||||
|
```
|
||||||
|
|
||||||
## Errors
|
## Errors
|
||||||
|
|
||||||
- `errors.VBANCMDError`: Exception raised when general errors occur.
|
- `errors.VBANCMDError`: Base VBANCMD Exception class.
|
||||||
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
||||||
|
|
||||||
## Logging
|
## Logging
|
||||||
@@ -524,18 +591,20 @@ import vban_cmd
|
|||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
opts = {"ip": "ip.local", "port": 6980, "streamname": "Command1"}
|
opts = {'ip': 'ip.local', 'port': 6980, 'streamname': 'Command1'}
|
||||||
with vban_cmd.api('banana', **opts) as vban:
|
with vban_cmd.api('banana', **opts) as vban:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tests
|
### Run tests
|
||||||
|
|
||||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
Install [poetry](https://python-poetry.org/docs/#installation) and then:
|
||||||
|
|
||||||
Then from tests directory:
|
```powershell
|
||||||
|
poetry poe test-basic
|
||||||
`pytest -v`
|
poetry poe test-banana
|
||||||
|
poetry poe test-potato
|
||||||
|
```
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
|||||||
21
__main__.py
21
__main__.py
@@ -6,27 +6,27 @@ class ManyThings:
|
|||||||
self.vban = vban
|
self.vban = vban
|
||||||
|
|
||||||
def things(self):
|
def things(self):
|
||||||
self.vban.strip[0].label = "podmic"
|
self.vban.strip[0].label = 'podmic'
|
||||||
self.vban.strip[0].mute = True
|
self.vban.strip[0].mute = True
|
||||||
print(
|
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):
|
def other_things(self):
|
||||||
self.vban.bus[3].gain = -6.3
|
self.vban.bus[3].gain = -6.3
|
||||||
self.vban.bus[4].eq = True
|
self.vban.bus[4].eq = True
|
||||||
info = (
|
info = (
|
||||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
f'bus 3 gain has been set to {self.vban.bus[3].gain}',
|
||||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
f'bus 4 eq has been set to {self.vban.bus[4].eq}',
|
||||||
)
|
)
|
||||||
print("\n".join(info))
|
print('\n'.join(info))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
kind_id = "banana"
|
KIND_ID = 'banana'
|
||||||
|
|
||||||
with vban_cmd.api(
|
with vban_cmd.api(
|
||||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
KIND_ID, ip='gamepc.local', port=6980, streamname='Command1'
|
||||||
) as vban:
|
) as vban:
|
||||||
do = ManyThings(vban)
|
do = ManyThings(vban)
|
||||||
do.things()
|
do.things()
|
||||||
@@ -35,11 +35,12 @@ def main():
|
|||||||
# set many parameters at once
|
# set many parameters at once
|
||||||
vban.apply(
|
vban.apply(
|
||||||
{
|
{
|
||||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
'strip-2': {'A1': True, 'B1': True, 'gain': -6.0},
|
||||||
"bus-2": {"mute": True},
|
'bus-2': {'mute': True},
|
||||||
|
'vban-in-0': {'on': True},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
|
|
||||||
logging.basicConfig(level=logging.DEBUG)
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import ttk
|
|
||||||
|
|
||||||
|
|
||||||
class App(tk.Tk):
|
class App(tk.Tk):
|
||||||
@@ -13,7 +14,7 @@ class App(tk.Tk):
|
|||||||
def __init__(self, vban):
|
def __init__(self, vban):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.vban = vban
|
self.vban = vban
|
||||||
self.title(f"{vban} - version {vban.version}")
|
self.title(f'{vban} - version {vban.version}')
|
||||||
self.vban.observer.add(self.on_ldirty)
|
self.vban.observer.add(self.on_ldirty)
|
||||||
|
|
||||||
# create widget variables
|
# create widget variables
|
||||||
@@ -24,10 +25,10 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
# initialize style table
|
# initialize style table
|
||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.style.theme_use("clam")
|
self.style.theme_use('clam')
|
||||||
self.style.configure(
|
self.style.configure(
|
||||||
"Mute.TButton",
|
'Mute.TButton',
|
||||||
foreground="#cd5c5c" if vban.strip[self.INDEX].mute else "#5a5a5a",
|
foreground='#cd5c5c' if vban.strip[self.INDEX].mute else '#5a5a5a',
|
||||||
)
|
)
|
||||||
|
|
||||||
# create labelframe and grid it onto the mainframe
|
# create labelframe and grid it onto the mainframe
|
||||||
@@ -39,7 +40,7 @@ class App(tk.Tk):
|
|||||||
self.labelframe,
|
self.labelframe,
|
||||||
from_=12,
|
from_=12,
|
||||||
to_=-60,
|
to_=-60,
|
||||||
orient="vertical",
|
orient='vertical',
|
||||||
variable=self.slider_var,
|
variable=self.slider_var,
|
||||||
command=lambda arg: self.on_slider_move(arg),
|
command=lambda arg: self.on_slider_move(arg),
|
||||||
)
|
)
|
||||||
@@ -47,15 +48,15 @@ class App(tk.Tk):
|
|||||||
column=0,
|
column=0,
|
||||||
row=0,
|
row=0,
|
||||||
)
|
)
|
||||||
slider.bind("<Double-Button-1>", self.on_button_double_click)
|
slider.bind('<Double-Button-1>', self.on_button_double_click)
|
||||||
|
|
||||||
# create level meter and grid it onto the labelframe
|
# create level meter and grid it onto the labelframe
|
||||||
level_meter = ttk.Progressbar(
|
level_meter = ttk.Progressbar(
|
||||||
self.labelframe,
|
self.labelframe,
|
||||||
orient="vertical",
|
orient='vertical',
|
||||||
variable=self.meter_var,
|
variable=self.meter_var,
|
||||||
maximum=72,
|
maximum=72,
|
||||||
mode="determinate",
|
mode='determinate',
|
||||||
)
|
)
|
||||||
level_meter.grid(column=1, row=0)
|
level_meter.grid(column=1, row=0)
|
||||||
|
|
||||||
@@ -66,8 +67,8 @@ class App(tk.Tk):
|
|||||||
# create button and grid it onto the labelframe
|
# create button and grid it onto the labelframe
|
||||||
button = ttk.Button(
|
button = ttk.Button(
|
||||||
self.labelframe,
|
self.labelframe,
|
||||||
text="Mute",
|
text='Mute',
|
||||||
style="Mute.TButton",
|
style='Mute.TButton',
|
||||||
command=lambda: self.on_button_press(),
|
command=lambda: self.on_button_press(),
|
||||||
)
|
)
|
||||||
button.grid(column=0, row=2, columnspan=2, padx=1, pady=2)
|
button.grid(column=0, row=2, columnspan=2, padx=1, pady=2)
|
||||||
@@ -83,7 +84,7 @@ class App(tk.Tk):
|
|||||||
self.button_var.set(not self.button_var.get())
|
self.button_var.set(not self.button_var.get())
|
||||||
self.vban.strip[self.INDEX].mute = self.button_var.get()
|
self.vban.strip[self.INDEX].mute = self.button_var.get()
|
||||||
self.style.configure(
|
self.style.configure(
|
||||||
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
'Mute.TButton', foreground='#cd5c5c' if self.button_var.get() else '#5a5a5a'
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_button_double_click(self, e):
|
def on_button_double_click(self, e):
|
||||||
@@ -100,10 +101,17 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with vban_cmd.api("banana", ldirty=True) as vban:
|
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 = App(vban)
|
||||||
app.mainloop()
|
app.mainloop()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import time
|
import os
|
||||||
|
import threading
|
||||||
from logging import config
|
from logging import config
|
||||||
|
|
||||||
import obsws_python as obsws
|
import obsws_python as obsws
|
||||||
@@ -7,85 +8,103 @@ import vban_cmd
|
|||||||
|
|
||||||
config.dictConfig(
|
config.dictConfig(
|
||||||
{
|
{
|
||||||
"version": 1,
|
'version': 1,
|
||||||
"formatters": {
|
'formatters': {
|
||||||
"standard": {
|
'standard': {
|
||||||
"format": "%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s"
|
'format': '%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"handlers": {
|
'handlers': {
|
||||||
"stream": {
|
'stream': {
|
||||||
"level": "DEBUG",
|
'level': 'DEBUG',
|
||||||
"class": "logging.StreamHandler",
|
'class': 'logging.StreamHandler',
|
||||||
"formatter": "standard",
|
'formatter': 'standard',
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loggers": {"vban_cmd.iremote": {"handlers": ["stream"], "level": "DEBUG"}},
|
'loggers': {
|
||||||
|
'vban_cmd.iremote': {
|
||||||
|
'handlers': ['stream'],
|
||||||
|
'level': 'DEBUG',
|
||||||
|
'propagate': False,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'root': {'handlers': ['stream'], 'level': 'WARNING'},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class Observer:
|
class Observer:
|
||||||
def __init__(self, vban):
|
def __init__(self, vban, stop_event):
|
||||||
self.vban = vban
|
self._vban = vban
|
||||||
self.client = obsws.EventClient()
|
self._stop_event = stop_event
|
||||||
self.client.callback.register(
|
self._client = obsws.EventClient()
|
||||||
|
self._client.callback.register(
|
||||||
(
|
(
|
||||||
self.on_current_program_scene_changed,
|
self.on_current_program_scene_changed,
|
||||||
self.on_exit_started,
|
self.on_exit_started,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.is_running = True
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||||
|
self._client.disconnect()
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
self.vban.strip[0].mute = True
|
self._vban.strip[0].mute = True
|
||||||
self.vban.strip[1].B1 = True
|
self._vban.strip[1].B1 = True
|
||||||
self.vban.strip[2].B2 = True
|
self._vban.strip[2].B2 = True
|
||||||
|
|
||||||
def on_brb(self):
|
def on_brb(self):
|
||||||
self.vban.strip[7].fadeto(0, 500)
|
self._vban.strip[7].fadeto(0, 500)
|
||||||
self.vban.bus[0].mute = True
|
self._vban.bus[0].mute = True
|
||||||
|
|
||||||
def on_end(self):
|
def on_end(self):
|
||||||
self.vban.apply(
|
self._vban.apply(
|
||||||
{
|
{
|
||||||
"strip-0": {"mute": True},
|
'strip-0': {'mute': True},
|
||||||
"strip-1": {"mute": True, "B1": False},
|
'strip-1': {'mute': True, 'B1': False},
|
||||||
"strip-2": {"mute": True, "B1": False},
|
'strip-2': {'mute': True, 'B1': False},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_live(self):
|
def on_live(self):
|
||||||
self.vban.strip[0].mute = False
|
self._vban.strip[0].mute = False
|
||||||
self.vban.strip[7].fadeto(-6, 500)
|
self._vban.strip[7].fadeto(-6, 500)
|
||||||
self.vban.strip[7].A3 = True
|
self._vban.strip[7].A3 = True
|
||||||
|
|
||||||
def on_current_program_scene_changed(self, data):
|
def on_current_program_scene_changed(self, data):
|
||||||
def fget(scene):
|
|
||||||
run = {
|
|
||||||
"START": self.on_start,
|
|
||||||
"BRB": self.on_brb,
|
|
||||||
"END": self.on_end,
|
|
||||||
"LIVE": self.on_live,
|
|
||||||
}
|
|
||||||
return run.get(scene)
|
|
||||||
|
|
||||||
scene = data.scene_name
|
scene = data.scene_name
|
||||||
print(f"Switched to scene {scene}")
|
print(f'Switched to scene {scene}')
|
||||||
if fn := fget(scene):
|
match scene:
|
||||||
fn()
|
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, _):
|
def on_exit_started(self, _):
|
||||||
self.client.unsubscribe()
|
self._stop_event.set()
|
||||||
self.is_running = False
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with vban_cmd.api("potato") as vban:
|
KIND_ID = 'potato'
|
||||||
observer = Observer(vban)
|
conn = {
|
||||||
while observer.is_running:
|
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||||
time.sleep(0.1)
|
'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__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name="obs",
|
name='obs',
|
||||||
description="OBS Example",
|
description='OBS Example',
|
||||||
install_requires=["obsws-python"],
|
install_requires=['obsws-python'],
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
|
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
|
|
||||||
@@ -13,23 +14,28 @@ class App:
|
|||||||
|
|
||||||
# define an 'on_update' callback function to receive event updates
|
# define an 'on_update' callback function to receive event updates
|
||||||
def on_update(self, event):
|
def on_update(self, event):
|
||||||
if event == "pdirty":
|
if event == 'pdirty':
|
||||||
print("pdirty!")
|
print('pdirty!')
|
||||||
elif event == "ldirty":
|
elif event == 'ldirty':
|
||||||
for bus in self.vban.bus:
|
for bus in self.vban.bus:
|
||||||
if bus.levels.isdirty:
|
if bus.levels.isdirty:
|
||||||
print(bus, bus.levels.all)
|
print(bus, bus.levels.all)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
KIND_ID = "banana"
|
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) as vban:
|
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True, **conn) as vban:
|
||||||
App(vban)
|
App(vban)
|
||||||
|
|
||||||
while cmd := input("Press <Enter> to exit\n"):
|
while _ := input('Press <Enter> to exit\n'):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
main()
|
main()
|
||||||
|
|||||||
429
poetry.lock
generated
429
poetry.lock
generated
@@ -1,304 +1,363 @@
|
|||||||
[[package]]
|
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||||
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.8.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"
|
|
||||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
|
||||||
|
|
||||||
[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]]
|
[[package]]
|
||||||
name = "cachetools"
|
name = "cachetools"
|
||||||
version = "5.3.1"
|
version = "5.5.0"
|
||||||
description = "Extensible memoizing collections and decorators"
|
description = "Extensible memoizing collections and decorators"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "chardet"
|
name = "chardet"
|
||||||
version = "5.1.0"
|
version = "5.2.0"
|
||||||
description = "Universal encoding detector for Python 3"
|
description = "Universal encoding detector for Python 3"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
[[package]]
|
files = [
|
||||||
name = "click"
|
{file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"},
|
||||||
version = "8.1.3"
|
{file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"},
|
||||||
description = "Composable command line interface toolkit"
|
]
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
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]]
|
[[package]]
|
||||||
name = "distlib"
|
name = "distlib"
|
||||||
version = "0.3.6"
|
version = "0.3.9"
|
||||||
description = "Distribution utilities"
|
description = "Distribution utilities"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.12.2"
|
version = "3.16.1"
|
||||||
description = "A platform independent file lock."
|
description = "A platform independent file lock."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
docs = ["furo (>=2024.8.6)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4.1)"]
|
||||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "1.1.1"
|
version = "2.0.0"
|
||||||
description = "iniconfig: brain-dead simple config-ini parsing"
|
description = "brain-dead simple config-ini parsing"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
[[package]]
|
files = [
|
||||||
name = "isort"
|
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
|
||||||
version = "5.10.1"
|
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
|
||||||
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 = "*"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "23.1"
|
version = "24.2"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
[[package]]
|
files = [
|
||||||
name = "pathspec"
|
{file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
|
||||||
version = "0.10.1"
|
{file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
]
|
||||||
category = "dev"
|
|
||||||
optional = false
|
|
||||||
python-versions = ">=3.7"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "3.7.0"
|
version = "4.3.6"
|
||||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
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-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
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]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
version = "1.0.0"
|
version = "1.5.0"
|
||||||
description = "plugin and hook calling mechanisms for python"
|
description = "plugin and hook calling mechanisms for python"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.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]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
dev = ["pre-commit", "tox"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "pyenv-inspect"
|
||||||
version = "1.11.0"
|
version = "0.4.0"
|
||||||
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
description = "An auxiliary library for the virtualenv-pyenv and tox-pyenv-redux plugins"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
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]]
|
[[package]]
|
||||||
name = "pyproject-api"
|
name = "pyproject-api"
|
||||||
version = "1.5.2"
|
version = "1.8.0"
|
||||||
description = "API to interact with the python pyproject.toml based projects"
|
description = "API to interact with the python pyproject.toml based projects"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.dependencies]
|
||||||
packaging = ">=23.1"
|
packaging = ">=24.1"
|
||||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
docs = ["furo (>=2024.8.6)", "sphinx-autodoc-typehints (>=2.4.1)"]
|
||||||
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytest-mock (>=3.14)", "setuptools (>=75.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.1.3"
|
version = "8.3.4"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.dependencies]
|
||||||
attrs = ">=19.2.0"
|
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||||
iniconfig = "*"
|
iniconfig = "*"
|
||||||
packaging = "*"
|
packaging = "*"
|
||||||
pluggy = ">=0.12,<2.0"
|
pluggy = ">=1.5,<2"
|
||||||
py = ">=1.8.2"
|
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||||
tomli = ">=1.0.0"
|
|
||||||
|
|
||||||
[package.extras]
|
[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]]
|
[[package]]
|
||||||
name = "pytest-randomly"
|
name = "pytest-randomly"
|
||||||
version = "3.12.0"
|
version = "3.16.0"
|
||||||
description = "Pytest plugin to randomly order tests and control random.seed."
|
description = "Pytest plugin to randomly order tests and control random.seed."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.dependencies]
|
||||||
pytest = "*"
|
pytest = "*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-repeat"
|
name = "ruff"
|
||||||
version = "0.9.1"
|
version = "0.9.2"
|
||||||
description = "pytest plugin for repeating tests"
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=3.7"
|
||||||
|
groups = ["dev"]
|
||||||
[package.dependencies]
|
files = [
|
||||||
pytest = ">=3.6"
|
{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]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.2.1"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
category = "main"
|
|
||||||
optional = false
|
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]]
|
[[package]]
|
||||||
name = "tox"
|
name = "tox"
|
||||||
version = "4.6.3"
|
version = "4.23.2"
|
||||||
description = "tox is a generic virtualenv management and test command line tool"
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.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]
|
[package.dependencies]
|
||||||
cachetools = ">=5.3.1"
|
cachetools = ">=5.5"
|
||||||
chardet = ">=5.1"
|
chardet = ">=5.2"
|
||||||
colorama = ">=0.4.6"
|
colorama = ">=0.4.6"
|
||||||
filelock = ">=3.12.2"
|
filelock = ">=3.16.1"
|
||||||
packaging = ">=23.1"
|
packaging = ">=24.1"
|
||||||
platformdirs = ">=3.5.3"
|
platformdirs = ">=4.3.6"
|
||||||
pluggy = ">=1"
|
pluggy = ">=1.5"
|
||||||
pyproject-api = ">=1.5.2"
|
pyproject-api = ">=1.8"
|
||||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||||
virtualenv = ">=20.23.1"
|
typing-extensions = {version = ">=4.12.2", markers = "python_version < \"3.11\""}
|
||||||
|
virtualenv = ">=20.26.6"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
test = ["devpi-process (>=1.0.2)", "pytest (>=8.3.3)", "pytest-mock (>=3.14)"]
|
||||||
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
|
||||||
|
[[package]]
|
||||||
|
name = "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]]
|
[[package]]
|
||||||
name = "virtualenv"
|
name = "virtualenv"
|
||||||
version = "20.23.1"
|
version = "20.29.0"
|
||||||
description = "Virtual Python Environment builder"
|
description = "Virtual Python Environment builder"
|
||||||
category = "dev"
|
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[package.dependencies]
|
||||||
distlib = ">=0.3.6,<1"
|
distlib = ">=0.3.7,<1"
|
||||||
filelock = ">=3.12,<4"
|
filelock = ">=3.12.2,<4"
|
||||||
platformdirs = ">=3.5.1,<4"
|
platformdirs = ">=3.9.1,<5"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
docs = ["furo (>=2023.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-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.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]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "2.1"
|
||||||
python-versions = "^3.10"
|
python-versions = ">=3.10"
|
||||||
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
content-hash = "13fc9d0eb15d5fc09b54c1c8cd8f528b260259e97ee6813b50ab4724c35d6677"
|
||||||
|
|
||||||
[metadata.files]
|
|
||||||
attrs = []
|
|
||||||
black = []
|
|
||||||
cachetools = []
|
|
||||||
chardet = []
|
|
||||||
click = []
|
|
||||||
colorama = []
|
|
||||||
distlib = []
|
|
||||||
filelock = []
|
|
||||||
iniconfig = []
|
|
||||||
isort = []
|
|
||||||
mypy-extensions = []
|
|
||||||
packaging = []
|
|
||||||
pathspec = []
|
|
||||||
platformdirs = []
|
|
||||||
pluggy = []
|
|
||||||
py = []
|
|
||||||
pyproject-api = []
|
|
||||||
pytest = []
|
|
||||||
pytest-randomly = []
|
|
||||||
pytest-repeat = []
|
|
||||||
tomli = []
|
|
||||||
tox = []
|
|
||||||
virtualenv = []
|
|
||||||
|
|||||||
141
pyproject.toml
141
pyproject.toml
@@ -1,43 +1,138 @@
|
|||||||
[tool.poetry]
|
[project]
|
||||||
name = "vban-cmd"
|
name = "vban-cmd"
|
||||||
version = "2.4.3"
|
version = "2.8.1"
|
||||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||||
license = "MIT"
|
license = { text = "MIT" }
|
||||||
readme = "README.md"
|
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]
|
[tool.poetry.requires-plugins]
|
||||||
python = "^3.10"
|
poethepoet = ">=0.42.0"
|
||||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
[tool.poetry.dev-dependencies]
|
pytest = "^8.3.4"
|
||||||
pytest = "^7.1.2"
|
pytest-randomly = "^3.16.0"
|
||||||
pytest-randomly = "^3.12.0"
|
ruff = "^0.9.2"
|
||||||
pytest-repeat = "^0.9.1"
|
tox = "^4.23.2"
|
||||||
black = "^22.3.0"
|
virtualenv-pyenv = "^0.5.0"
|
||||||
isort = "^5.10.1"
|
|
||||||
tox = "^4.6.3"
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poe]
|
||||||
gui = "scripts:ex_gui"
|
envfile = ".env"
|
||||||
obs = "scripts:ex_obs"
|
|
||||||
observer = "scripts:ex_observer"
|
[tool.poe.tasks]
|
||||||
test = "scripts:test"
|
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]
|
[tool.tox]
|
||||||
legacy_tox_ini = """
|
legacy_tox_ini = """
|
||||||
[tox]
|
[tox]
|
||||||
envlist = py310,py311
|
envlist = py310,py311,py312,py313
|
||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
|
passenv = *
|
||||||
|
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||||
allowlist_externals = poetry
|
allowlist_externals = poetry
|
||||||
commands =
|
commands =
|
||||||
poetry install -v
|
poetry install -v
|
||||||
poetry run pytest tests/
|
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"]
|
||||||
|
|||||||
25
scripts.py
25
scripts.py
@@ -1,22 +1,35 @@
|
|||||||
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
def ex_gui():
|
def ex_gui():
|
||||||
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
scriptpath = Path.cwd() / 'examples' / 'gui' / '.'
|
||||||
subprocess.run([sys.executable, str(scriptpath)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_obs():
|
def ex_obs():
|
||||||
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
subprocess.run(['tox', 'r', '-e', 'obs'])
|
||||||
subprocess.run([sys.executable, str(scriptpath)])
|
|
||||||
|
|
||||||
|
|
||||||
def ex_observer():
|
def ex_observer():
|
||||||
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
scriptpath = Path.cwd() / 'examples' / 'observer' / '.'
|
||||||
subprocess.run([sys.executable, str(scriptpath)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def test():
|
def test_basic():
|
||||||
subprocess.run(["tox"])
|
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,3 +1,4 @@
|
|||||||
|
import os
|
||||||
import random
|
import random
|
||||||
import sys
|
import sys
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
@@ -6,14 +7,13 @@ import vban_cmd
|
|||||||
from vban_cmd.kinds import KindId
|
from vban_cmd.kinds import KindId
|
||||||
from vban_cmd.kinds import request_kind_map as kindmap
|
from vban_cmd.kinds import request_kind_map as kindmap
|
||||||
|
|
||||||
# let's keep things random
|
# get KIND from environment, if not set default to potato
|
||||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
KIND_ID = os.environ.get('KIND', 'potato')
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
"ip": "testing.local",
|
'ip': os.getenv('VBANCMD_IP', 'localhost'),
|
||||||
"streamname": "testing",
|
'streamname': os.getenv('VBANCMD_STREAMNAME', 'Command1'),
|
||||||
"port": 6990,
|
'port': int(os.getenv('VBANCMD_PORT', 6980)),
|
||||||
"bps": 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vban = vban_cmd.api(KIND_ID, **opts)
|
vban = vban_cmd.api(KIND_ID, **opts)
|
||||||
@@ -39,7 +39,7 @@ data = Data()
|
|||||||
|
|
||||||
|
|
||||||
def setup_module():
|
def setup_module():
|
||||||
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
print(f'\nRunning tests for kind [{data.name}]\n', file=sys.stdout)
|
||||||
vban.login()
|
vban.login()
|
||||||
vban.command.reset()
|
vban.command.reset()
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
def pytest_addoption(parser):
|
||||||
parser.addoption(
|
parser.addoption(
|
||||||
"--run-slow",
|
'--run-slow',
|
||||||
action="store_true",
|
action='store_true',
|
||||||
default=False,
|
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
|
$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"
|
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 ".") {
|
if ($MyInvocation.InvocationName -ne ".") {
|
||||||
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
||||||
|
|
||||||
|
@("potato") | ForEach-Object {
|
||||||
|
$env:KIND = $_
|
||||||
RunTests
|
RunTests
|
||||||
|
}
|
||||||
|
|
||||||
Invoke-Expression "deactivate"
|
Invoke-Expression "deactivate"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests import data, vban
|
from tests import data, vban
|
||||||
@@ -10,18 +12,27 @@ class TestSetAndGetBoolHigher:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
vban.apply_config("example")
|
vban.apply_config('example')
|
||||||
|
time.sleep(0.1)
|
||||||
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
|
|
||||||
|
|
||||||
def test_it_tests_config_bool(self):
|
|
||||||
assert vban.strip[0].A1 == True
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"not config.getoption('--run-slow')",
|
"not config.getoption('--run-slow')",
|
||||||
reason="Only run when --run-slow is given",
|
reason='Only run when --run-slow is given',
|
||||||
|
)
|
||||||
|
def test_it_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):
|
def test_it_tests_config_busmode(self):
|
||||||
assert vban.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)
|
||||||
@@ -7,15 +7,15 @@ class TestRemoteFactories:
|
|||||||
__test__ = True
|
__test__ = True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "basic",
|
data.name != 'basic',
|
||||||
reason="Skip test if kind is not basic",
|
reason='Skip test if kind is not basic',
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_basic(self):
|
def test_it_tests_remote_attrs_for_basic(self):
|
||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, 'strip')
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, 'bus')
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, 'command')
|
||||||
assert hasattr(vban, "button")
|
assert hasattr(vban, 'button')
|
||||||
assert hasattr(vban, "vban")
|
assert hasattr(vban, 'vban')
|
||||||
|
|
||||||
assert len(vban.strip) == 3
|
assert len(vban.strip) == 3
|
||||||
assert len(vban.bus) == 2
|
assert len(vban.bus) == 2
|
||||||
@@ -23,15 +23,15 @@ class TestRemoteFactories:
|
|||||||
assert len(vban.vban.instream) == 6 and len(vban.vban.outstream) == 5
|
assert len(vban.vban.instream) == 6 and len(vban.vban.outstream) == 5
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != 'banana',
|
||||||
reason="Skip test if kind is not basic",
|
reason='Skip test if kind is not basic',
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_banana(self):
|
def test_it_tests_remote_attrs_for_banana(self):
|
||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, 'strip')
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, 'bus')
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, 'command')
|
||||||
assert hasattr(vban, "button")
|
assert hasattr(vban, 'button')
|
||||||
assert hasattr(vban, "vban")
|
assert hasattr(vban, 'vban')
|
||||||
|
|
||||||
assert len(vban.strip) == 5
|
assert len(vban.strip) == 5
|
||||||
assert len(vban.bus) == 5
|
assert len(vban.bus) == 5
|
||||||
@@ -39,15 +39,15 @@ class TestRemoteFactories:
|
|||||||
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != 'potato',
|
||||||
reason="Skip test if kind is not basic",
|
reason='Skip test if kind is not basic',
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_potato(self):
|
def test_it_tests_remote_attrs_for_potato(self):
|
||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, 'strip')
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, 'bus')
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, 'command')
|
||||||
assert hasattr(vban, "button")
|
assert hasattr(vban, 'button')
|
||||||
assert hasattr(vban, "vban")
|
assert hasattr(vban, 'vban')
|
||||||
|
|
||||||
assert len(vban.strip) == 8
|
assert len(vban.strip) == 8
|
||||||
assert len(vban.bus) == 8
|
assert len(vban.bus) == 8
|
||||||
|
|||||||
@@ -3,17 +3,17 @@ import pytest
|
|||||||
from tests import data, vban
|
from tests import data, vban
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", [False, True])
|
@pytest.mark.parametrize('value', [False, True])
|
||||||
class TestSetAndGetBoolHigher:
|
class TestSetAndGetBoolHigher:
|
||||||
__test__ = True
|
__test__ = True
|
||||||
|
|
||||||
"""strip tests, physical and virtual"""
|
"""strip tests, physical and virtual"""
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param",
|
'index,param',
|
||||||
[
|
[
|
||||||
(data.phys_in, "mute"),
|
(data.phys_in, 'mute'),
|
||||||
(data.virt_in, "solo"),
|
(data.virt_in, 'solo'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||||
@@ -21,13 +21,13 @@ class TestSetAndGetBoolHigher:
|
|||||||
assert getattr(vban.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name == "banana",
|
data.name == 'banana',
|
||||||
reason="Only test if logged into Basic or Potato version",
|
reason='Only test if logged into Basic or Potato version',
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@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):
|
def test_it_sets_and_gets_strip_bool_params_mc(self, index, param, value):
|
||||||
@@ -37,10 +37,9 @@ class TestSetAndGetBoolHigher:
|
|||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param",
|
'index,param',
|
||||||
[
|
[
|
||||||
(data.phys_out, "mute"),
|
(data.phys_out, 'mute'),
|
||||||
(data.virt_out, "sel"),
|
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
||||||
@@ -51,17 +50,17 @@ class TestSetAndGetBoolHigher:
|
|||||||
""" bus modes tests, physical and virtual """
|
""" bus modes tests, physical and virtual """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param",
|
'index,param',
|
||||||
[
|
[
|
||||||
(data.phys_out, "normal"),
|
(data.phys_out, 'normal'),
|
||||||
(data.phys_out, "amix"),
|
(data.phys_out, 'amix'),
|
||||||
(data.phys_out, "rearonly"),
|
(data.phys_out, 'rearonly'),
|
||||||
(data.virt_out, "normal"),
|
(data.virt_out, 'normal'),
|
||||||
(data.virt_out, "upmix41"),
|
(data.virt_out, 'upmix41'),
|
||||||
(data.virt_out, "composite"),
|
(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
|
# here it only makes sense to set/get bus modes as True
|
||||||
if not value:
|
if not value:
|
||||||
value = True
|
value = True
|
||||||
@@ -71,8 +70,8 @@ class TestSetAndGetBoolHigher:
|
|||||||
""" command tests """
|
""" command tests """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"param",
|
'param',
|
||||||
[("lock")],
|
[('lock')],
|
||||||
)
|
)
|
||||||
def test_it_sets_command_bool_params(self, param, value):
|
def test_it_sets_command_bool_params(self, param, value):
|
||||||
setattr(vban.command, param, value)
|
setattr(vban.command, param, value)
|
||||||
@@ -86,10 +85,10 @@ class TestSetAndGetIntHigher:
|
|||||||
"""strip tests, physical and virtual"""
|
"""strip tests, physical and virtual"""
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param,value",
|
'index,param,value',
|
||||||
[
|
[
|
||||||
(data.virt_in, "k", 0),
|
(data.virt_in, 'k', 0),
|
||||||
(data.virt_in, "k", 4),
|
(data.virt_in, 'k', 4),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||||
@@ -103,12 +102,12 @@ class TestSetAndGetFloatHigher:
|
|||||||
"""strip tests, physical and virtual"""
|
"""strip tests, physical and virtual"""
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param,value",
|
'index,param,value',
|
||||||
[
|
[
|
||||||
(data.phys_in, "gain", -3.6),
|
(data.phys_in, 'gain', -3.6),
|
||||||
(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.virt_in, "gain", 5.8),
|
(data.virt_in, 'gain', 5.8),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
||||||
@@ -116,18 +115,20 @@ class TestSetAndGetFloatHigher:
|
|||||||
assert getattr(vban.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,value",
|
'index,value',
|
||||||
[(data.phys_in, 2), (data.phys_in, 2), (data.virt_in, 8), (data.virt_in, 8)],
|
[(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):
|
def test_it_gets_strip_prefader_levels_and_compares_length_of_array(
|
||||||
|
self, index, value
|
||||||
|
):
|
||||||
assert len(vban.strip[index].levels.prefader) == value
|
assert len(vban.strip[index].levels.prefader) == value
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != 'potato',
|
||||||
reason="Only test if logged into Potato version",
|
reason='Only test if logged into Potato version',
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, j, value",
|
'index, j, value',
|
||||||
[
|
[
|
||||||
(data.phys_in, 0, -20.7),
|
(data.phys_in, 0, -20.7),
|
||||||
(data.virt_in, 3, -60),
|
(data.virt_in, 3, -60),
|
||||||
@@ -142,14 +143,14 @@ class TestSetAndGetFloatHigher:
|
|||||||
""" strip tests, physical """
|
""" strip tests, physical """
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != 'potato',
|
||||||
reason="Only test if logged into Potato version",
|
reason='Only test if logged into Potato version',
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param, value",
|
'index, param, value',
|
||||||
[
|
[
|
||||||
(data.phys_in, "gainin", -8.6),
|
(data.phys_in, 'gainin', -8.6),
|
||||||
(data.phys_in, "knee", 0.24),
|
(data.phys_in, 'knee', 0.24),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_strip_comp_params(self, index, param, value):
|
def test_it_sets_strip_comp_params(self, index, param, value):
|
||||||
@@ -158,14 +159,14 @@ class TestSetAndGetFloatHigher:
|
|||||||
# we can set but not get this value. Not in RT Packet.
|
# we can set but not get this value. Not in RT Packet.
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != 'potato',
|
||||||
reason="Only test if logged into Potato version",
|
reason='Only test if logged into Potato version',
|
||||||
)
|
)
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param, value",
|
'index, param, value',
|
||||||
[
|
[
|
||||||
(data.phys_in, "bpsidechain", 120),
|
(data.phys_in, 'bpsidechain', 120),
|
||||||
(data.phys_in, "hold", 3000),
|
(data.phys_in, 'hold', 3000),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_gate_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_gate_params(self, index, param, value):
|
||||||
@@ -175,12 +176,13 @@ class TestSetAndGetFloatHigher:
|
|||||||
|
|
||||||
""" strip tests, virtual """
|
""" strip tests, virtual """
|
||||||
|
|
||||||
|
@pytest.mark.skip(reason='Requires RT Packet NBS 1')
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param, value",
|
'index, param, value',
|
||||||
[
|
[
|
||||||
(data.virt_in, "treble", -1.6),
|
(data.virt_in, 'treble', -1.6),
|
||||||
(data.virt_in, "mid", 5.8),
|
(data.virt_in, 'mid', 5.8),
|
||||||
(data.virt_in, "bass", -8.1),
|
(data.virt_in, 'bass', -8.1),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
||||||
@@ -190,30 +192,30 @@ class TestSetAndGetFloatHigher:
|
|||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param, value",
|
'index, param, value',
|
||||||
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
[(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):
|
def test_it_sets_and_gets_bus_float_params(self, index, param, value):
|
||||||
setattr(vban.bus[index], param, value)
|
setattr(vban.bus[index], param, value)
|
||||||
assert getattr(vban.bus[index], param) == value
|
assert getattr(vban.bus[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,value",
|
'index,value',
|
||||||
[(data.phys_out, 8), (data.virt_out, 8)],
|
[(data.phys_out, 8), (data.virt_out, 8)],
|
||||||
)
|
)
|
||||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
def test_it_gets_bus_levels_and_compares_length_of_array(self, index, value):
|
||||||
assert len(vban.bus[index].levels.all) == value
|
assert len(vban.bus[index].levels.all) == value
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", ["test0", "test1"])
|
@pytest.mark.parametrize('value', ['test0', 'test1'])
|
||||||
class TestSetAndGetStringHigher:
|
class TestSetAndGetStringHigher:
|
||||||
__test__ = True
|
__test__ = True
|
||||||
|
|
||||||
"""strip tests, physical and virtual"""
|
"""strip tests, physical and virtual"""
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param",
|
'index, param',
|
||||||
[(data.phys_in, "label"), (data.virt_in, "label")],
|
[(data.phys_in, 'label'), (data.virt_in, 'label')],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
||||||
setattr(vban.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
@@ -222,8 +224,8 @@ class TestSetAndGetStringHigher:
|
|||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index, param",
|
'index, param',
|
||||||
[(data.phys_out, "label"), (data.virt_out, "label")],
|
[(data.phys_out, 'label'), (data.virt_out, 'label')],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
||||||
setattr(vban.bus[index], param, value)
|
setattr(vban.bus[index], param, value)
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests import data, vban
|
from tests import data, vban
|
||||||
@@ -11,31 +9,26 @@ class TestPublicPacketLower:
|
|||||||
|
|
||||||
"""Tests for a valid rt data packet"""
|
"""Tests for a valid rt data packet"""
|
||||||
|
|
||||||
def test_it_gets_an_rt_data_packet(self):
|
def test_it_gets_an_rt0_data_packet(self):
|
||||||
assert vban.public_packet.voicemeetertype in (
|
assert vban.public_packets[0].voicemeetertype in (
|
||||||
kind.name for kind in kinds.kinds_all
|
kind.name for kind in kinds.all
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.parametrize('value', [0, 1])
|
||||||
"not config.getoption('--run-slow')",
|
|
||||||
reason="Only run when --run-slow is given",
|
|
||||||
)
|
|
||||||
@pytest.mark.parametrize("value", [0, 1])
|
|
||||||
class TestSetRT:
|
class TestSetRT:
|
||||||
__test__ = True
|
__test__ = True
|
||||||
|
|
||||||
"""Tests set_rt"""
|
"""Tests set_rt"""
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"kls,index,param",
|
'kls,index,param',
|
||||||
[
|
[
|
||||||
("strip", data.phys_in, "mute"),
|
('strip', data.phys_in, 'mute'),
|
||||||
("bus", data.virt_out, "mono"),
|
('bus', data.virt_out, 'mono'),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sends_a_text_request(self, kls, index, param, value):
|
def test_it_sends_a_text_request(self, kls, index, param, value):
|
||||||
vban._set_rt(f"{kls}[{index}]", param, value)
|
vban._set_rt(f'{kls}[{index}].{param}', value)
|
||||||
time.sleep(0.02)
|
|
||||||
target = getattr(vban, kls)[index]
|
target = getattr(vban, kls)[index]
|
||||||
assert getattr(target, param) == bool(value)
|
assert getattr(target, param) == bool(value)
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
from .factory import request_vbancmd_obj as api
|
from .factory import request_vbancmd_obj as api
|
||||||
|
|
||||||
__ALL__ = ["api"]
|
__ALL__ = ['api']
|
||||||
|
|||||||
133
vban_cmd/bus.py
133
vban_cmd/bus.py
@@ -1,16 +1,10 @@
|
|||||||
|
import abc
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
|
||||||
from enum import IntEnum
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from .enums import NBS, BusModes
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .meta import bus_mode_prop, channel_bool_prop, channel_label_prop
|
from .meta import bus_mode_prop, channel_bool_prop, channel_int_prop, channel_label_prop
|
||||||
|
|
||||||
BusModes = IntEnum(
|
|
||||||
"BusModes",
|
|
||||||
"normal amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly",
|
|
||||||
start=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class Bus(IRemote):
|
class Bus(IRemote):
|
||||||
@@ -20,35 +14,32 @@ class Bus(IRemote):
|
|||||||
Defines concrete implementation for bus
|
Defines concrete implementation for bus
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Bus[{self.index}]"
|
return f'bus[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gain(self) -> float:
|
def gain(self) -> float:
|
||||||
def fget():
|
val = self.getter('gain')
|
||||||
val = self.public_packet.busgain[self.index]
|
if val:
|
||||||
if 0 <= val <= 1200:
|
return round(val, 2)
|
||||||
return val * 0.01
|
else:
|
||||||
return (((1 << 16) - 1) - val) * -0.01
|
return self.public_packets[NBS.zero].busgain[self.index]
|
||||||
|
|
||||||
val = self.getter("gain")
|
|
||||||
return round(val if val else fget(), 1)
|
|
||||||
|
|
||||||
@gain.setter
|
@gain.setter
|
||||||
def gain(self, val: float):
|
def gain(self, val: float):
|
||||||
self.setter("gain", val)
|
self.setter('gain', val)
|
||||||
|
|
||||||
def fadeto(self, target: float, time_: int):
|
def fadeto(self, target: float, time_: int):
|
||||||
self.setter("FadeTo", f"({target}, {time_})")
|
self.setter('FadeTo', f'({target}, {time_})')
|
||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|
||||||
def fadeby(self, change: float, time_: int):
|
def fadeby(self, change: float, time_: int):
|
||||||
self.setter("FadeBy", f"({change}, {time_})")
|
self.setter('FadeBy', f'({change}, {time_})')
|
||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|
||||||
|
|
||||||
@@ -56,22 +47,22 @@ class BusEQ(IRemote):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, remote, index):
|
def make(cls, remote, index):
|
||||||
BUSEQ_cls = type(
|
BUSEQ_cls = type(
|
||||||
f"BusEQ{remote.kind}",
|
f'BusEQ{remote.kind}',
|
||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
**{param: channel_bool_prop(param) for param in ["on", "ab"]},
|
**{param: channel_bool_prop(param) for param in ['on', 'ab']},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return BUSEQ_cls(remote, index)
|
return BUSEQ_cls(remote, index)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Bus[{self.index}].eq"
|
return f'bus[{self.index}].eq'
|
||||||
|
|
||||||
|
|
||||||
class PhysicalBus(Bus):
|
class PhysicalBus(Bus):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f'{type(self).__name__}{self.index}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device(self) -> str:
|
def device(self) -> str:
|
||||||
@@ -84,7 +75,7 @@ class PhysicalBus(Bus):
|
|||||||
|
|
||||||
class VirtualBus(Bus):
|
class VirtualBus(Bus):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f'{type(self).__name__}{self.index}'
|
||||||
|
|
||||||
|
|
||||||
class BusLevel(IRemote):
|
class BusLevel(IRemote):
|
||||||
@@ -99,24 +90,13 @@ class BusLevel(IRemote):
|
|||||||
def getter(self):
|
def getter(self):
|
||||||
"""Returns a tuple of level values for the channel."""
|
"""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:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return self._remote.cache['bus_level'][self.range[0] : self.range[-1]]
|
||||||
fget(i)
|
return self.public_packets[NBS.zero].levels.bus[self.range[0] : self.range[-1]]
|
||||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
|
||||||
)
|
|
||||||
return tuple(
|
|
||||||
fget(i)
|
|
||||||
for i in self._remote._get_levels(self.public_packet)[1][
|
|
||||||
self.range[0] : self.range[-1]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Bus[{self.index}]"
|
return f'bus[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def all(self) -> tuple:
|
def all(self) -> tuple:
|
||||||
@@ -137,37 +117,51 @@ class BusLevel(IRemote):
|
|||||||
def _make_bus_mode_mixin():
|
def _make_bus_mode_mixin():
|
||||||
"""Creates a mixin of Bus Modes."""
|
"""Creates a mixin of Bus Modes."""
|
||||||
|
|
||||||
|
mode_names = [
|
||||||
|
'normal',
|
||||||
|
'amix',
|
||||||
|
'repeat',
|
||||||
|
'bmix',
|
||||||
|
'composite',
|
||||||
|
'tvmix',
|
||||||
|
'upmix21',
|
||||||
|
'upmix41',
|
||||||
|
'upmix61',
|
||||||
|
'centeronly',
|
||||||
|
'lfeonly',
|
||||||
|
'rearonly',
|
||||||
|
]
|
||||||
|
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Bus[{self.index}].mode"
|
return f'bus[{self.index}].mode'
|
||||||
|
|
||||||
def get(self):
|
def get(self):
|
||||||
time.sleep(0.01)
|
"""Get current bus mode using ChannelState for clean bit extraction."""
|
||||||
for i, val in enumerate(
|
mode_cache_items = [
|
||||||
[
|
(k, v)
|
||||||
self.amix,
|
for k, v in self._remote.cache.items()
|
||||||
self.bmix,
|
if k.startswith(f'{self.identifier}.') and v == 1
|
||||||
self.repeat,
|
|
||||||
self.composite,
|
|
||||||
self.tvmix,
|
|
||||||
self.upmix21,
|
|
||||||
self.upmix41,
|
|
||||||
self.upmix61,
|
|
||||||
self.centeronly,
|
|
||||||
self.lfeonly,
|
|
||||||
self.rearonly,
|
|
||||||
]
|
]
|
||||||
):
|
|
||||||
if val:
|
if mode_cache_items:
|
||||||
return BusModes(i + 1).name
|
latest_cached = mode_cache_items[-1][0]
|
||||||
return "normal"
|
mode_name = latest_cached.split('.')[-1]
|
||||||
|
return mode_name
|
||||||
|
|
||||||
|
bus_state = self.public_packets[NBS.zero].states.bus[self.index]
|
||||||
|
|
||||||
|
# Extract bus mode from bits 4-7 (mask 0xF0, shift right by 4)
|
||||||
|
mode_value = (bus_state._state & 0x000000F0) >> 4
|
||||||
|
|
||||||
|
return mode_names[mode_value] if mode_value < len(mode_names) else 'normal'
|
||||||
|
|
||||||
return type(
|
return type(
|
||||||
"BusModeMixin",
|
'BusModeMixin',
|
||||||
(IRemote,),
|
(IRemote,),
|
||||||
{
|
{
|
||||||
"identifier": property(identifier),
|
'identifier': property(identifier),
|
||||||
**{mode.name: bus_mode_prop(mode.name) for mode in BusModes},
|
**{mode.name: bus_mode_prop(mode.name) for mode in BusModes},
|
||||||
"get": get,
|
'get': get,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -181,14 +175,15 @@ def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
|||||||
BUS_cls = PhysicalBus if phys_bus else VirtualBus
|
BUS_cls = PhysicalBus if phys_bus else VirtualBus
|
||||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||||
return type(
|
return type(
|
||||||
f"{BUS_cls.__name__}{remote.kind}",
|
f'{BUS_cls.__name__}{remote.kind}',
|
||||||
(BUS_cls,),
|
(BUS_cls,),
|
||||||
{
|
{
|
||||||
"eq": BusEQ.make(remote, i),
|
'eq': BusEQ.make(remote, i),
|
||||||
"levels": BusLevel(remote, i),
|
'levels': BusLevel(remote, i),
|
||||||
"mode": BUSMODEMIXIN_cls(remote, i),
|
'mode': BUSMODEMIXIN_cls(remote, i),
|
||||||
**{param: channel_bool_prop(param) for param in ["mute", "mono"]},
|
**{param: channel_bool_prop(param) for param in ('mute',)},
|
||||||
"label": channel_label_prop(),
|
**{param: channel_int_prop(param) for param in ('mono',)},
|
||||||
|
'label': channel_label_prop(),
|
||||||
},
|
},
|
||||||
)(remote, i)
|
)(remote, i)
|
||||||
|
|
||||||
|
|||||||
@@ -17,30 +17,30 @@ class Command(IRemote):
|
|||||||
Returns a Command class of a kind.
|
Returns a Command class of a kind.
|
||||||
"""
|
"""
|
||||||
CMD_cls = type(
|
CMD_cls = type(
|
||||||
f"Command{remote.kind}",
|
f'Command{remote.kind}',
|
||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
**{
|
**{
|
||||||
param: action_fn(param) for param in ["show", "shutdown", "restart"]
|
param: action_fn(param) for param in ['show', 'shutdown', 'restart']
|
||||||
},
|
},
|
||||||
"hide": action_fn("show", val=0),
|
'hide': action_fn('show', val=0),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return CMD_cls(remote)
|
return CMD_cls(remote)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return "Command"
|
return 'command'
|
||||||
|
|
||||||
def set_showvbanchat(self, val: bool):
|
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)
|
showvbanchat = property(fset=set_showvbanchat)
|
||||||
|
|
||||||
def set_lock(self, val: bool):
|
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)
|
lock = property(fset=set_lock)
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self._remote.apply_config("reset")
|
self._remote.apply_config('reset')
|
||||||
|
|||||||
@@ -20,73 +20,73 @@ class TOMLStrBuilder:
|
|||||||
def __init__(self, kind):
|
def __init__(self, kind):
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self.higher = itertools.chain(
|
self.higher = itertools.chain(
|
||||||
[f"strip-{i}" for i in range(kind.num_strip)],
|
[f'strip-{i}' for i in range(kind.num_strip)],
|
||||||
[f"bus-{i}" for i in range(kind.num_bus)],
|
[f'bus-{i}' for i in range(kind.num_bus)],
|
||||||
)
|
)
|
||||||
|
|
||||||
def init_config(self, profile=None):
|
def init_config(self, profile=None):
|
||||||
self.virt_strip_params = (
|
self.virt_strip_params = (
|
||||||
[
|
[
|
||||||
"mute = false",
|
'mute = false',
|
||||||
"mono = false",
|
'mono = false',
|
||||||
"solo = false",
|
'solo = false',
|
||||||
"gain = 0.0",
|
'gain = 0.0',
|
||||||
]
|
]
|
||||||
+ [f"A{i} = false" for i in range(1, self.kind.phys_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)]
|
+ [f'B{i} = false' for i in range(1, self.kind.virt_out + 1)]
|
||||||
)
|
)
|
||||||
self.phys_strip_params = self.virt_strip_params + [
|
self.phys_strip_params = self.virt_strip_params + [
|
||||||
"comp.knob = 0.0",
|
'comp.knob = 0.0',
|
||||||
"gate.knob = 0.0",
|
'gate.knob = 0.0',
|
||||||
"denoiser.knob = 0.0",
|
'denoiser.knob = 0.0',
|
||||||
"eq.on = false",
|
'eq.on = false',
|
||||||
]
|
]
|
||||||
self.bus_float = ["gain = 0.0"]
|
self.bus_float = ['gain = 0.0']
|
||||||
self.bus_params = [
|
self.bus_params = [
|
||||||
"mono = false",
|
'mono = false',
|
||||||
"eq.on = false",
|
'eq.on = false',
|
||||||
"mute = false",
|
'mute = false',
|
||||||
"gain = 0.0",
|
'gain = 0.0',
|
||||||
]
|
]
|
||||||
|
|
||||||
if profile == "reset":
|
if profile == 'reset':
|
||||||
self.reset_config()
|
self.reset_config()
|
||||||
|
|
||||||
def reset_config(self):
|
def reset_config(self):
|
||||||
self.phys_strip_params = list(
|
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(
|
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)
|
self.init_config(profile)
|
||||||
toml_str = str()
|
toml_str = str()
|
||||||
for eachclass in self.higher:
|
for eachclass in self.higher:
|
||||||
toml_str += f"[{eachclass}]\n"
|
toml_str += f'[{eachclass}]\n'
|
||||||
toml_str = self.join(eachclass, toml_str)
|
toml_str = self.join(eachclass, toml_str)
|
||||||
return toml_str
|
return toml_str
|
||||||
|
|
||||||
def join(self, eachclass, toml_str):
|
def join(self, eachclass, toml_str):
|
||||||
kls, index = eachclass.split("-")
|
kls, index = eachclass.split('-')
|
||||||
match kls:
|
match kls:
|
||||||
case "strip":
|
case 'strip':
|
||||||
toml_str += ("\n").join(
|
toml_str += ('\n').join(
|
||||||
self.phys_strip_params
|
self.phys_strip_params
|
||||||
if int(index) < self.kind.phys_in
|
if int(index) < self.kind.phys_in
|
||||||
else self.virt_strip_params
|
else self.virt_strip_params
|
||||||
)
|
)
|
||||||
case "bus":
|
case 'bus':
|
||||||
toml_str += ("\n").join(self.bus_params)
|
toml_str += ('\n').join(self.bus_params)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
return toml_str + "\n"
|
return toml_str + '\n'
|
||||||
|
|
||||||
|
|
||||||
class TOMLDataExtractor:
|
class TOMLDataExtractor:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
with open(file, "rb") as f:
|
with open(file, 'rb') as f:
|
||||||
self._data = tomllib.load(f)
|
self._data = tomllib.load(f)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -104,10 +104,10 @@ def dataextraction_factory(file):
|
|||||||
|
|
||||||
this opens the possibility for other parsers to be added
|
this opens the possibility for other parsers to be added
|
||||||
"""
|
"""
|
||||||
if file.suffix == ".toml":
|
if file.suffix == '.toml':
|
||||||
extractor = TOMLDataExtractor
|
extractor = TOMLDataExtractor
|
||||||
else:
|
else:
|
||||||
raise ValueError("Cannot extract data from {}".format(file))
|
raise ValueError('Cannot extract data from {}'.format(file))
|
||||||
return extractor(file)
|
return extractor(file)
|
||||||
|
|
||||||
|
|
||||||
@@ -141,20 +141,25 @@ class Loader(metaclass=SingletonType):
|
|||||||
def defaults(self, kind):
|
def defaults(self, kind):
|
||||||
self.builder = TOMLStrBuilder(kind)
|
self.builder = TOMLStrBuilder(kind)
|
||||||
toml_str = self.builder.build()
|
toml_str = self.builder.build()
|
||||||
self.register("reset", tomllib.loads(toml_str))
|
self.register('reset', tomllib.loads(toml_str))
|
||||||
|
|
||||||
def parse(self, identifier, data):
|
def parse(self, identifier, data):
|
||||||
if identifier in self._configs:
|
if identifier in self._configs:
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
f"config file with name {identifier} already in memory, skipping.."
|
f'config file with name {identifier} already in memory, skipping..'
|
||||||
)
|
)
|
||||||
return False
|
return
|
||||||
|
try:
|
||||||
self.parser = dataextraction_factory(data)
|
self.parser = dataextraction_factory(data)
|
||||||
|
except tomllib.TOMLDecodeError as e:
|
||||||
|
ERR_MSG = (str(e), f'When attempting to load {identifier}.toml')
|
||||||
|
self.logger.error(f'{type(e).__name__}: {" ".join(ERR_MSG)}')
|
||||||
|
return
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def register(self, identifier, data=None):
|
def register(self, identifier, data=None):
|
||||||
self._configs[identifier] = data if data else self.parser.data
|
self._configs[identifier] = data if data else self.parser.data
|
||||||
self.logger.info(f"config {self.name}/{identifier} loaded into memory")
|
self.logger.info(f'config {self.name}/{identifier} loaded into memory')
|
||||||
|
|
||||||
def deregister(self):
|
def deregister(self):
|
||||||
self._configs.clear()
|
self._configs.clear()
|
||||||
@@ -177,18 +182,18 @@ def loader(kind):
|
|||||||
|
|
||||||
returns configs loaded into memory
|
returns configs loaded into memory
|
||||||
"""
|
"""
|
||||||
logger_loader = logger.getChild("loader")
|
logger_loader = logger.getChild('loader')
|
||||||
loader = Loader(kind)
|
loader = Loader(kind)
|
||||||
|
|
||||||
for path in (
|
for path in (
|
||||||
Path.cwd() / "configs" / kind.name,
|
Path.cwd() / 'configs' / kind.name,
|
||||||
Path.home() / ".config" / "vban-cmd" / kind.name,
|
Path.home() / '.config' / 'vban-cmd' / kind.name,
|
||||||
Path.home() / "Documents" / "Voicemeeter" / "configs" / kind.name,
|
Path.home() / 'Documents' / 'Voicemeeter' / 'configs' / kind.name,
|
||||||
):
|
):
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
logger_loader.info(f"Checking [{path}] for TOML config files:")
|
logger_loader.info(f'Checking [{path}] for TOML config files:')
|
||||||
for file in path.glob("*.toml"):
|
for file in path.glob('*.toml'):
|
||||||
identifier = file.with_suffix("").stem
|
identifier = file.with_suffix('').stem
|
||||||
if loader.parse(identifier, file):
|
if loader.parse(identifier, file):
|
||||||
loader.register(identifier)
|
loader.register(identifier)
|
||||||
return loader.configs
|
return loader.configs
|
||||||
@@ -203,5 +208,5 @@ def request_config(kind_id: str):
|
|||||||
try:
|
try:
|
||||||
configs = loader(kindmap(kind_id))
|
configs = loader(kindmap(kind_id))
|
||||||
except KeyError:
|
except KeyError:
|
||||||
raise VBANCMDError(f"Unknown Voicemeeter kind {kind_id}")
|
raise VBANCMDError(f'Unknown Voicemeeter kind {kind_id}')
|
||||||
return configs
|
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,5 +1,5 @@
|
|||||||
class VBANCMDError(Exception):
|
class VBANCMDError(Exception):
|
||||||
"""Base VBANCMD Exception class. Raised when general errors occur"""
|
"""Base VBANCMD Exception class."""
|
||||||
|
|
||||||
|
|
||||||
class VBANCMDConnectionError(VBANCMDError):
|
class VBANCMDConnectionError(VBANCMDError):
|
||||||
|
|||||||
@@ -12,30 +12,30 @@ class Event:
|
|||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
def info(self, msg=None):
|
def info(self, msg=None):
|
||||||
info = (f"{msg} events",) if msg else ()
|
info = (f'{msg} events',) if msg else ()
|
||||||
if self.any():
|
if self.any():
|
||||||
info += (f"now listening for {', '.join(self.get())} events",)
|
info += (f'now listening for {", ".join(self.get())} events',)
|
||||||
else:
|
else:
|
||||||
info += (f"not listening for any events",)
|
info += ('not listening for any events',)
|
||||||
self.logger.info(", ".join(info))
|
self.logger.info(', '.join(info))
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pdirty(self) -> bool:
|
def pdirty(self) -> bool:
|
||||||
return self.subs["pdirty"]
|
return self.subs['pdirty']
|
||||||
|
|
||||||
@pdirty.setter
|
@pdirty.setter
|
||||||
def pdirty(self, val: bool):
|
def pdirty(self, val: bool):
|
||||||
self.subs["pdirty"] = val
|
self.subs['pdirty'] = val
|
||||||
self.info(f"pdirty {'added to' if val else 'removed from'}")
|
self.info(f'pdirty {"added to" if val else "removed from"}')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ldirty(self) -> bool:
|
def ldirty(self) -> bool:
|
||||||
return self.subs["ldirty"]
|
return self.subs['ldirty']
|
||||||
|
|
||||||
@ldirty.setter
|
@ldirty.setter
|
||||||
def ldirty(self, val: bool):
|
def ldirty(self, val: bool):
|
||||||
self.subs["ldirty"] = val
|
self.subs['ldirty'] = val
|
||||||
self.info(f"ldirty {'added to' if val else 'removed from'}")
|
self.info(f'ldirty {"added to" if val else "removed from"}')
|
||||||
|
|
||||||
def get(self) -> list:
|
def get(self) -> list:
|
||||||
return [k for k, v in self.subs.items() if v]
|
return [k for k, v in self.subs.items() if v]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
|
import abc
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
@@ -11,6 +11,7 @@ from .error import VBANCMDError
|
|||||||
from .kinds import KindMapClass
|
from .kinds import KindMapClass
|
||||||
from .kinds import request_kind_map as kindmap
|
from .kinds import request_kind_map as kindmap
|
||||||
from .macrobutton import MacroButton
|
from .macrobutton import MacroButton
|
||||||
|
from .recorder import Recorder
|
||||||
from .strip import request_strip_obj as strip
|
from .strip import request_strip_obj as strip
|
||||||
from .vban import request_vban_obj as vban
|
from .vban import request_vban_obj as vban
|
||||||
from .vbancmd import VbanCmd
|
from .vbancmd import VbanCmd
|
||||||
@@ -26,25 +27,26 @@ class FactoryBuilder:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
BuilderProgress = IntEnum(
|
BuilderProgress = IntEnum(
|
||||||
"BuilderProgress", "strip bus command macrobutton vban", start=0
|
'BuilderProgress', 'strip bus command macrobutton vban recorder', start=0
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(self, factory, kind: KindMapClass):
|
def __init__(self, factory, kind: KindMapClass):
|
||||||
self._factory = factory
|
self._factory = factory
|
||||||
self.kind = kind
|
self.kind = kind
|
||||||
self._info = (
|
self._info = (
|
||||||
f"Finished building strips for {self._factory}",
|
f'Finished building strips for {self._factory}',
|
||||||
f"Finished building buses for {self._factory}",
|
f'Finished building buses for {self._factory}',
|
||||||
f"Finished building commands for {self._factory}",
|
f'Finished building commands for {self._factory}',
|
||||||
f"Finished building macrobuttons for {self._factory}",
|
f'Finished building macrobuttons for {self._factory}',
|
||||||
f"Finished building vban in/out streams for {self._factory}",
|
f'Finished building vban in/out streams for {self._factory}',
|
||||||
|
f'Finished building recorder for {self._factory}',
|
||||||
)
|
)
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
def _pinfo(self, name: str) -> None:
|
def _pinfo(self, name: str) -> None:
|
||||||
"""prints progress status for each step"""
|
"""prints progress status for each step"""
|
||||||
name = name.split("_")[1]
|
name = name.split('_')[1]
|
||||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
self.logger.debug(self._info[int(getattr(self.BuilderProgress, name))])
|
||||||
|
|
||||||
def make_strip(self):
|
def make_strip(self):
|
||||||
self._factory.strip = tuple(
|
self._factory.strip = tuple(
|
||||||
@@ -72,26 +74,30 @@ class FactoryBuilder:
|
|||||||
self._factory.vban = vban(self._factory)
|
self._factory.vban = vban(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def make_recorder(self):
|
||||||
|
self._factory.recorder = Recorder.make(self._factory)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class FactoryBase(VbanCmd):
|
class FactoryBase(VbanCmd):
|
||||||
"""Base class for factories, subclasses VbanCmd."""
|
"""Base class for factories, subclasses VbanCmd."""
|
||||||
|
|
||||||
def __init__(self, kind_id: str, **kwargs):
|
def __init__(self, kind_id: str, **kwargs):
|
||||||
defaultkwargs = {
|
defaultkwargs = {
|
||||||
"ip": None,
|
'ip': 'localhost',
|
||||||
"port": 6980,
|
'port': 6980,
|
||||||
"streamname": "Command1",
|
'streamname': 'Command1',
|
||||||
"bps": 0,
|
'bps': 256000,
|
||||||
"channel": 0,
|
'channel': 0,
|
||||||
"ratelimit": 0.01,
|
'ratelimit': 0.01,
|
||||||
"timeout": 5,
|
'timeout': 5,
|
||||||
"outbound": False,
|
'disable_rt_listeners': False,
|
||||||
"sync": False,
|
'sync': False,
|
||||||
"pdirty": False,
|
'pdirty': False,
|
||||||
"ldirty": False,
|
'ldirty': False,
|
||||||
}
|
}
|
||||||
if "subs" in kwargs:
|
if 'subs' in kwargs:
|
||||||
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
defaultkwargs |= kwargs.pop('subs') # for backwards compatibility
|
||||||
kwargs = defaultkwargs | kwargs
|
kwargs = defaultkwargs | kwargs
|
||||||
self.kind = kindmap(kind_id)
|
self.kind = kindmap(kind_id)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -106,7 +112,7 @@ class FactoryBase(VbanCmd):
|
|||||||
self._configs = None
|
self._configs = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Voicemeeter {self.kind}"
|
return f'Voicemeeter {self.kind}'
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +121,7 @@ class FactoryBase(VbanCmd):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def steps(self):
|
def steps(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -166,7 +172,7 @@ class BananaFactory(FactoryBase):
|
|||||||
@property
|
@property
|
||||||
def steps(self) -> Iterable:
|
def steps(self) -> Iterable:
|
||||||
"""steps required to build the interface for a kind"""
|
"""steps required to build the interface for a kind"""
|
||||||
return self._steps
|
return self._steps + (self.builder.make_recorder,)
|
||||||
|
|
||||||
|
|
||||||
class PotatoFactory(FactoryBase):
|
class PotatoFactory(FactoryBase):
|
||||||
@@ -188,7 +194,7 @@ class PotatoFactory(FactoryBase):
|
|||||||
@property
|
@property
|
||||||
def steps(self) -> Iterable:
|
def steps(self) -> Iterable:
|
||||||
"""steps required to build the interface for a kind"""
|
"""steps required to build the interface for a kind"""
|
||||||
return self._steps
|
return self._steps + (self.builder.make_recorder,)
|
||||||
|
|
||||||
|
|
||||||
def vbancmd_factory(kind_id: str, **kwargs) -> VbanCmd:
|
def vbancmd_factory(kind_id: str, **kwargs) -> VbanCmd:
|
||||||
@@ -198,15 +204,21 @@ def vbancmd_factory(kind_id: str, **kwargs) -> VbanCmd:
|
|||||||
Returns a VbanCmd class of a kind
|
Returns a VbanCmd class of a kind
|
||||||
"""
|
"""
|
||||||
match kind_id:
|
match kind_id:
|
||||||
case "basic":
|
case 'basic':
|
||||||
_factory = BasicFactory
|
_factory = BasicFactory
|
||||||
case "banana":
|
case 'banana':
|
||||||
_factory = BananaFactory
|
_factory = BananaFactory
|
||||||
case "potato":
|
case 'potato' | 'matrix':
|
||||||
|
# matrix is a special kind where:
|
||||||
|
# - we don't need to scale the interface with the builder (in other words kind is arbitrary).
|
||||||
|
# - we don't ever need to use real-time listeners, so we disable them to avoid confusion
|
||||||
|
if kind_id == 'matrix':
|
||||||
|
kwargs['disable_rt_listeners'] = True
|
||||||
|
kind_id = 'potato'
|
||||||
_factory = PotatoFactory
|
_factory = PotatoFactory
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unknown Voicemeeter kind '{kind_id}'")
|
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:
|
def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
||||||
@@ -215,12 +227,12 @@ def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
|||||||
|
|
||||||
Returns a reference to a VbanCmd class of a kind
|
Returns a reference to a VbanCmd class of a kind
|
||||||
"""
|
"""
|
||||||
logger_entry = logger.getChild("factory.request_vbancmd_obj")
|
logger_entry = logger.getChild('factory.request_vbancmd_obj')
|
||||||
|
|
||||||
VBANCMD_obj = None
|
VBANCMD_obj = None
|
||||||
try:
|
try:
|
||||||
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
logger_entry.exception(f"{type(e).__name__}: {e}")
|
logger_entry.exception(f'{type(e).__name__}: {e}')
|
||||||
raise VBANCMDError(str(e)) from e
|
raise VBANCMDError(str(e)) from e
|
||||||
return VBANCMD_obj
|
return VBANCMD_obj
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
|
import abc
|
||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -78,7 +78,7 @@ class Modes:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class IRemote(metaclass=ABCMeta):
|
class IRemote(abc.ABC):
|
||||||
"""
|
"""
|
||||||
Common interface between base class and extended (higher) classes
|
Common interface between base class and extended (higher) classes
|
||||||
|
|
||||||
@@ -93,7 +93,7 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
|
|
||||||
def getter(self, param):
|
def getter(self, param):
|
||||||
cmd = self._cmd(param)
|
cmd = self._cmd(param)
|
||||||
self.logger.debug(f"getter: {cmd}")
|
self.logger.debug(f'getter: {cmd}')
|
||||||
if cmd in self._remote.cache:
|
if cmd in self._remote.cache:
|
||||||
return self._remote.cache.pop(cmd)
|
return self._remote.cache.pop(cmd)
|
||||||
if self._remote.sync:
|
if self._remote.sync:
|
||||||
@@ -101,33 +101,33 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
|
|
||||||
def setter(self, param, val):
|
def setter(self, param, val):
|
||||||
"""Sends a string request RT packet."""
|
"""Sends a string request RT packet."""
|
||||||
self.logger.debug(f"setter: {self._cmd(param)}={val}")
|
self.logger.debug(f'setter: {self._cmd(param)}={val}')
|
||||||
self._remote._set_rt(self._cmd(param), val)
|
self._remote._set_rt(self._cmd(param), val)
|
||||||
|
|
||||||
def _cmd(self, param):
|
def _cmd(self, param):
|
||||||
cmd = (self.identifier,)
|
cmd = (self.identifier,)
|
||||||
if param:
|
if param:
|
||||||
cmd += (f".{param}",)
|
cmd += (f'.{param}',)
|
||||||
return "".join(cmd)
|
return ''.join(cmd)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_packet(self):
|
def public_packets(self):
|
||||||
"""Returns an RT data packet."""
|
"""Returns an RT data packet."""
|
||||||
return self._remote.public_packet
|
return self._remote.public_packets
|
||||||
|
|
||||||
def apply(self, data):
|
def apply(self, data):
|
||||||
"""Sets all parameters of a dict for the channel."""
|
"""Sets all parameters of a dict for the channel."""
|
||||||
|
|
||||||
def fget(attr, val):
|
def fget(attr, val):
|
||||||
if attr == "mode":
|
if attr == 'mode':
|
||||||
return (f"mode.{val}", 1)
|
return (f'mode.{val}', 1)
|
||||||
elif attr == "knob":
|
elif attr == 'knob':
|
||||||
return ("", val)
|
return ('', val)
|
||||||
return (attr, val)
|
return (attr, val)
|
||||||
|
|
||||||
for attr, val in data.items():
|
for attr, val in data.items():
|
||||||
@@ -138,7 +138,7 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
val = 1 if val else 0
|
val = 1 if val else 0
|
||||||
|
|
||||||
self._remote.cache[self._cmd(attr)] = val
|
self._remote.cache[self._cmd(attr)] = val
|
||||||
self._remote._script += f"{self._cmd(attr)}={val};"
|
self._remote._script += f'{self._cmd(attr)}={val};'
|
||||||
else:
|
else:
|
||||||
target = getattr(self, attr)
|
target = getattr(self, attr)
|
||||||
target.apply(val)
|
target.apply(val)
|
||||||
|
|||||||
@@ -1,16 +1,9 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, unique
|
|
||||||
|
|
||||||
|
from .enums import KindId
|
||||||
from .error import VBANCMDError
|
from .error import VBANCMDError
|
||||||
|
|
||||||
|
|
||||||
@unique
|
|
||||||
class KindId(Enum):
|
|
||||||
BASIC = 1
|
|
||||||
BANANA = 2
|
|
||||||
POTATO = 3
|
|
||||||
|
|
||||||
|
|
||||||
class SingletonType(type):
|
class SingletonType(type):
|
||||||
"""ensure only a single instance of a kind map object"""
|
"""ensure only a single instance of a kind map object"""
|
||||||
|
|
||||||
@@ -22,12 +15,15 @@ class SingletonType(type):
|
|||||||
return cls._instances[cls]
|
return cls._instances[cls]
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class KindMapClass(metaclass=SingletonType):
|
class KindMapClass(metaclass=SingletonType):
|
||||||
name: str
|
name: str
|
||||||
ins: tuple
|
ins: tuple
|
||||||
outs: tuple
|
outs: tuple
|
||||||
vban: tuple
|
vban: tuple
|
||||||
|
strip_channels: int
|
||||||
|
bus_channels: int
|
||||||
|
cells: int
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def phys_in(self):
|
def phys_in(self):
|
||||||
@@ -65,40 +61,49 @@ class KindMapClass(metaclass=SingletonType):
|
|||||||
return self.name.capitalize()
|
return self.name.capitalize()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class BasicMap(KindMapClass):
|
class BasicMap(KindMapClass):
|
||||||
name: str
|
name: str
|
||||||
ins: tuple = (2, 1)
|
ins: tuple = (2, 1)
|
||||||
outs: tuple = (1, 1)
|
outs: tuple = (1, 1)
|
||||||
vban: tuple = (4, 4, 1, 1)
|
vban: tuple = (4, 4, 1, 1)
|
||||||
|
strip_channels: int = 0
|
||||||
|
bus_channels: int = 0
|
||||||
|
cells: int = 0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class BananaMap(KindMapClass):
|
class BananaMap(KindMapClass):
|
||||||
name: str
|
name: str
|
||||||
ins: tuple = (3, 2)
|
ins: tuple = (3, 2)
|
||||||
outs: tuple = (3, 2)
|
outs: tuple = (3, 2)
|
||||||
vban: tuple = (8, 8, 1, 1)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
strip_channels: int = 0
|
||||||
|
bus_channels: int = 8
|
||||||
|
cells: int = 6
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass(frozen=True)
|
||||||
class PotatoMap(KindMapClass):
|
class PotatoMap(KindMapClass):
|
||||||
name: str
|
name: str
|
||||||
ins: tuple = (5, 3)
|
ins: tuple = (5, 3)
|
||||||
outs: tuple = (5, 3)
|
outs: tuple = (5, 3)
|
||||||
vban: tuple = (8, 8, 1, 1)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
strip_channels: int = 2
|
||||||
|
bus_channels: int = 8
|
||||||
|
cells: int = 6
|
||||||
|
|
||||||
|
|
||||||
def kind_factory(kind_id):
|
def kind_factory(kind_id):
|
||||||
match kind_id:
|
match kind_id:
|
||||||
case "basic":
|
case 'basic':
|
||||||
_kind_map = BasicMap
|
_kind_map = BasicMap
|
||||||
case "banana":
|
case 'banana':
|
||||||
_kind_map = BananaMap
|
_kind_map = BananaMap
|
||||||
case "potato":
|
case 'potato':
|
||||||
_kind_map = PotatoMap
|
_kind_map = PotatoMap
|
||||||
case _:
|
case _:
|
||||||
raise ValueError(f"Unknown Voicemeeter kind {kind_id}")
|
raise ValueError(f'Unknown Voicemeeter kind {kind_id}')
|
||||||
return _kind_map(name=kind_id)
|
return _kind_map(name=kind_id)
|
||||||
|
|
||||||
|
|
||||||
@@ -111,4 +116,4 @@ def request_kind_map(kind_id):
|
|||||||
return KIND_obj
|
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)
|
||||||
|
|||||||
@@ -5,32 +5,32 @@ class MacroButton(IRemote):
|
|||||||
"""A placeholder class in case this interface is being used interchangeably with the Remote API"""
|
"""A placeholder class in case this interface is being used interchangeably with the Remote API"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
return f'{type(self).__name__}{self._remote.kind}{self.index}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
return f"command.button[{self.index}]"
|
return f'command.button[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> bool:
|
def state(self) -> bool:
|
||||||
self.logger.warning("button.state commands are not supported over VBAN")
|
self.logger.warning('button.state commands are not supported over VBAN')
|
||||||
|
|
||||||
@state.setter
|
@state.setter
|
||||||
def state(self, _):
|
def state(self, _):
|
||||||
self.logger.warning("button.state commands are not supported over VBAN")
|
self.logger.warning('button.state commands are not supported over VBAN')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stateonly(self) -> bool:
|
def stateonly(self) -> bool:
|
||||||
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
self.logger.warning('button.stateonly commands are not supported over VBAN')
|
||||||
|
|
||||||
@stateonly.setter
|
@stateonly.setter
|
||||||
def stateonly(self, v):
|
def stateonly(self, v):
|
||||||
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
self.logger.warning('button.stateonly commands are not supported over VBAN')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def trigger(self) -> bool:
|
def trigger(self) -> bool:
|
||||||
self.logger.warning("button.trigger commands are not supported over VBAN")
|
self.logger.warning('button.trigger commands are not supported over VBAN')
|
||||||
|
|
||||||
@trigger.setter
|
@trigger.setter
|
||||||
def trigger(self, _):
|
def trigger(self, _):
|
||||||
self.logger.warning("button.trigger commands are not supported over VBAN")
|
self.logger.warning('button.trigger commands are not supported over VBAN')
|
||||||
|
|||||||
171
vban_cmd/meta.py
171
vban_cmd/meta.py
@@ -1,6 +1,7 @@
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from .util import cache_bool, cache_string
|
from .enums import NBS, BusModes
|
||||||
|
from .util import cache_bool, cache_float, cache_int, cache_string
|
||||||
|
|
||||||
|
|
||||||
def channel_bool_prop(param):
|
def channel_bool_prop(param):
|
||||||
@@ -8,17 +9,25 @@ def channel_bool_prop(param):
|
|||||||
|
|
||||||
@partial(cache_bool, param=param)
|
@partial(cache_bool, param=param)
|
||||||
def fget(self):
|
def fget(self):
|
||||||
return (
|
cmd = self._cmd(param)
|
||||||
not int.from_bytes(
|
self.logger.debug(f'getter: {cmd}')
|
||||||
getattr(
|
|
||||||
self.public_packet,
|
states = self.public_packets[NBS.zero].states
|
||||||
f"{'strip' if 'strip' in type(self).__name__.lower() else 'bus'}state",
|
channel_states = (
|
||||||
)[self.index],
|
states.strip if 'strip' in type(self).__name__.lower() else states.bus
|
||||||
"little",
|
|
||||||
)
|
|
||||||
& getattr(self._modes, f"_{param.lower()}")
|
|
||||||
== 0
|
|
||||||
)
|
)
|
||||||
|
channel_state = channel_states[self.index]
|
||||||
|
|
||||||
|
if param.lower() == 'mute':
|
||||||
|
return channel_state.mute
|
||||||
|
elif param.lower() == 'solo':
|
||||||
|
return channel_state.solo
|
||||||
|
elif param.lower() == 'mono':
|
||||||
|
return channel_state.mono
|
||||||
|
elif param.lower() == 'mc':
|
||||||
|
return channel_state.mc
|
||||||
|
else:
|
||||||
|
return channel_state.get_mode(getattr(self._modes, f'_{param.lower()}'))
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
self.setter(param, 1 if val else 0)
|
self.setter(param, 1 if val else 0)
|
||||||
@@ -26,18 +35,46 @@ def channel_bool_prop(param):
|
|||||||
return property(fget, fset)
|
return property(fget, fset)
|
||||||
|
|
||||||
|
|
||||||
|
def channel_int_prop(param):
|
||||||
|
"""meta function for channel integer parameters"""
|
||||||
|
|
||||||
|
@partial(cache_int, param=param)
|
||||||
|
def fget(self):
|
||||||
|
cmd = self._cmd(param)
|
||||||
|
self.logger.debug(f'getter: {cmd}')
|
||||||
|
|
||||||
|
states = self.public_packets[NBS.zero].states
|
||||||
|
channel_states = (
|
||||||
|
states.strip if 'strip' in type(self).__name__.lower() else states.bus
|
||||||
|
)
|
||||||
|
channel_state = channel_states[self.index]
|
||||||
|
|
||||||
|
# Special case: bus mono is an integer (0-2) encoded using bits 2 and 9
|
||||||
|
if param.lower() == 'mono' and 'bus' in type(self).__name__.lower():
|
||||||
|
bit_2 = (channel_state._state >> 2) & 1
|
||||||
|
bit_9 = (channel_state._state >> 9) & 1
|
||||||
|
return (bit_9 << 1) | bit_2
|
||||||
|
else:
|
||||||
|
return channel_state.get_mode_int(getattr(self._modes, f'_{param.lower()}'))
|
||||||
|
|
||||||
|
def fset(self, val):
|
||||||
|
self.setter(param, val)
|
||||||
|
|
||||||
|
return property(fget, fset)
|
||||||
|
|
||||||
|
|
||||||
def channel_label_prop():
|
def channel_label_prop():
|
||||||
"""meta function for channel label parameters"""
|
"""meta function for channel label parameters"""
|
||||||
|
|
||||||
@partial(cache_string, param="label")
|
@partial(cache_string, param='label')
|
||||||
def fget(self) -> str:
|
def fget(self) -> str:
|
||||||
return getattr(
|
if 'strip' in type(self).__name__.lower():
|
||||||
self.public_packet,
|
return self.public_packets[NBS.zero].labels.strip[self.index]
|
||||||
f"{'strip' if 'strip' in type(self).__name__.lower() else 'bus'}labels",
|
else:
|
||||||
)[self.index]
|
return self.public_packets[NBS.zero].labels.bus[self.index]
|
||||||
|
|
||||||
def fset(self, val: str):
|
def fset(self, val: str):
|
||||||
self.setter("label", str(val))
|
self.setter('label', f'"{val}"')
|
||||||
|
|
||||||
return property(fget, fset)
|
return property(fget, fset)
|
||||||
|
|
||||||
@@ -47,11 +84,12 @@ def strip_output_prop(param):
|
|||||||
|
|
||||||
@partial(cache_bool, param=param)
|
@partial(cache_bool, param=param)
|
||||||
def fget(self):
|
def fget(self):
|
||||||
return (
|
cmd = self._cmd(param)
|
||||||
not int.from_bytes(self.public_packet.stripstate[self.index], "little")
|
self.logger.debug(f'getter: {cmd}')
|
||||||
& getattr(self._modes, f"_bus{param.lower()}")
|
|
||||||
== 0
|
strip_state = self.public_packets[NBS.zero].states.strip[self.index]
|
||||||
)
|
|
||||||
|
return strip_state.get_mode(getattr(self._modes, f'_bus{param.lower()}'))
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
self.setter(param, 1 if val else 0)
|
self.setter(param, 1 if val else 0)
|
||||||
@@ -64,26 +102,17 @@ def bus_mode_prop(param):
|
|||||||
|
|
||||||
@partial(cache_bool, param=param)
|
@partial(cache_bool, param=param)
|
||||||
def fget(self):
|
def fget(self):
|
||||||
modelist = {
|
cmd = self._cmd(param)
|
||||||
"amix": (1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1),
|
self.logger.debug(f'getter: {cmd}')
|
||||||
"repeat": (0, 2, 2, 0, 0, 2, 2, 0, 0, 2, 2),
|
|
||||||
"bmix": (1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3),
|
bus_state = self.public_packets[NBS.zero].states.bus[self.index]
|
||||||
"composite": (0, 0, 0, 4, 4, 4, 4, 0, 0, 0, 0),
|
|
||||||
"tvmix": (1, 0, 1, 4, 5, 4, 5, 0, 1, 0, 1),
|
# Extract current bus mode from bits 4-7
|
||||||
"upmix21": (0, 2, 2, 4, 4, 6, 6, 0, 0, 2, 2),
|
current_mode = (bus_state._state & 0x000000F0) >> 4
|
||||||
"upmix41": (1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3),
|
|
||||||
"upmix61": (0, 0, 0, 0, 0, 0, 0, 8, 8, 8, 8),
|
expected_mode = getattr(BusModes, param.lower())
|
||||||
"centeronly": (1, 0, 1, 0, 1, 0, 1, 8, 9, 8, 9),
|
|
||||||
"lfeonly": (0, 2, 2, 0, 0, 2, 2, 8, 8, 10, 10),
|
return current_mode == expected_mode
|
||||||
"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
|
|
||||||
)
|
|
||||||
if param == "normal":
|
|
||||||
return not any(vals)
|
|
||||||
return tuple(round(val / 16) for val in vals) == modelist[param]
|
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
self.setter(param, 1 if val else 0)
|
self.setter(param, 1 if val else 0)
|
||||||
@@ -98,3 +127,61 @@ def action_fn(param, val=1):
|
|||||||
self.setter(param, val)
|
self.setter(param, val)
|
||||||
|
|
||||||
return fdo
|
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,302 +0,0 @@
|
|||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from .kinds import KindMapClass
|
|
||||||
from .util import comp
|
|
||||||
|
|
||||||
VBAN_PROTOCOL_TXT = 0x40
|
|
||||||
VBAN_PROTOCOL_SERVICE = 0x60
|
|
||||||
|
|
||||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
|
||||||
VBAN_SERVICE_RTPACKET = 33
|
|
||||||
|
|
||||||
MAX_PACKET_SIZE = 1436
|
|
||||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VbanRtPacket:
|
|
||||||
"""Represents the body of a VBAN RT data packet"""
|
|
||||||
|
|
||||||
_kind: KindMapClass
|
|
||||||
_voicemeeterType: bytes # data[28:29]
|
|
||||||
_reserved: bytes # data[29:30]
|
|
||||||
_buffersize: bytes # data[30:32]
|
|
||||||
_voicemeeterVersion: bytes # data[32:36]
|
|
||||||
_optionBits: bytes # data[36:40]
|
|
||||||
_samplerate: bytes # data[40:44]
|
|
||||||
_inputLeveldB100: bytes # data[44:112]
|
|
||||||
_outputLeveldB100: bytes # data[112:240]
|
|
||||||
_TransportBit: bytes # data[240:244]
|
|
||||||
_stripState: bytes # data[244:276]
|
|
||||||
_busState: bytes # data[276:308]
|
|
||||||
_stripGaindB100Layer1: bytes # data[308:324]
|
|
||||||
_stripGaindB100Layer2: bytes # data[324:340]
|
|
||||||
_stripGaindB100Layer3: bytes # data[340:356]
|
|
||||||
_stripGaindB100Layer4: bytes # data[356:372]
|
|
||||||
_stripGaindB100Layer5: bytes # data[372:388]
|
|
||||||
_stripGaindB100Layer6: bytes # data[388:404]
|
|
||||||
_stripGaindB100Layer7: bytes # data[404:420]
|
|
||||||
_stripGaindB100Layer8: bytes # data[420:436]
|
|
||||||
_busGaindB100: bytes # data[436:452]
|
|
||||||
_stripLabelUTF8c60: bytes # data[452:932]
|
|
||||||
_busLabelUTF8c60: bytes # data[932:1412]
|
|
||||||
|
|
||||||
def _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._stripGaindB100Layer1 == other._stripGaindB100Layer1
|
|
||||||
and self._stripGaindB100Layer2 == other._stripGaindB100Layer2
|
|
||||||
and self._stripGaindB100Layer3 == other._stripGaindB100Layer3
|
|
||||||
and self._stripGaindB100Layer4 == other._stripGaindB100Layer4
|
|
||||||
and self._stripGaindB100Layer5 == other._stripGaindB100Layer5
|
|
||||||
and self._stripGaindB100Layer6 == other._stripGaindB100Layer6
|
|
||||||
and self._stripGaindB100Layer7 == other._stripGaindB100Layer7
|
|
||||||
and self._stripGaindB100Layer8 == other._stripGaindB100Layer8
|
|
||||||
and self._busGaindB100 == other._busGaindB100
|
|
||||||
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
|
|
||||||
and self._busLabelUTF8c60 == other._busLabelUTF8c60
|
|
||||||
)
|
|
||||||
|
|
||||||
def ldirty(self, strip_cache, bus_cache) -> bool:
|
|
||||||
self._strip_comp, self._bus_comp = (
|
|
||||||
tuple(not val for val in comp(strip_cache, self.strip_levels)),
|
|
||||||
tuple(not val for val in comp(bus_cache, self.bus_levels)),
|
|
||||||
)
|
|
||||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def voicemeetertype(self) -> str:
|
|
||||||
"""returns voicemeeter type as a string"""
|
|
||||||
type_ = ("basic", "banana", "potato")
|
|
||||||
return type_[int.from_bytes(self._voicemeeterType, "little") - 1]
|
|
||||||
|
|
||||||
@property
|
|
||||||
def voicemeeterversion(self) -> tuple:
|
|
||||||
"""returns voicemeeter version as a tuple"""
|
|
||||||
return tuple(
|
|
||||||
reversed(
|
|
||||||
tuple(
|
|
||||||
int.from_bytes(self._voicemeeterVersion[i : i + 1], "little")
|
|
||||||
for i in range(4)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def samplerate(self) -> int:
|
|
||||||
"""returns samplerate as an int"""
|
|
||||||
return int.from_bytes(self._samplerate, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
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) -> 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:
|
|
||||||
"""returns tuple of strip states accessable through bit modes"""
|
|
||||||
return tuple(self._stripState[i : i + 4] for i in range(0, 32, 4))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def busstate(self) -> tuple:
|
|
||||||
"""returns tuple of bus states accessable through bit modes"""
|
|
||||||
return tuple(self._busState[i : i + 4] for i in range(0, 32, 4))
|
|
||||||
|
|
||||||
"""
|
|
||||||
these functions return an array of gainlayers[i] across all strips
|
|
||||||
ie stripgainlayer1 = [strip[0].gainlayer[0], strip[1].gainlayer[0], strip[2].gainlayer[0]...]
|
|
||||||
"""
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer1(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer2(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer2[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer3(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer3[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer4(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer4[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer5(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer5[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer6(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer6[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer7(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer7[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def stripgainlayer8(self) -> tuple:
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._stripGaindB100Layer8[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def busgain(self) -> tuple:
|
|
||||||
"""returns tuple of bus gains"""
|
|
||||||
return tuple(
|
|
||||||
int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
|
||||||
for i in range(0, 16, 2)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def striplabels(self) -> tuple:
|
|
||||||
"""returns tuple of strip labels"""
|
|
||||||
return tuple(
|
|
||||||
self._stripLabelUTF8c60[i : i + 60].decode().split("\x00")[0]
|
|
||||||
for i in range(0, 480, 60)
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def buslabels(self) -> tuple:
|
|
||||||
"""returns tuple of bus labels"""
|
|
||||||
return tuple(
|
|
||||||
self._busLabelUTF8c60[i : i + 60].decode().split("\x00")[0]
|
|
||||||
for i in range(0, 480, 60)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SubscribeHeader:
|
|
||||||
"""Represents the header an RT Packet Service subscription packet"""
|
|
||||||
|
|
||||||
name = "Register RTP"
|
|
||||||
timeout = 15
|
|
||||||
vban: bytes = "VBAN".encode()
|
|
||||||
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
|
||||||
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
|
||||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
|
||||||
framecounter: bytes = (0).to_bytes(4, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def header(self):
|
|
||||||
header = self.vban
|
|
||||||
header += self.format_sr
|
|
||||||
header += self.format_nbs
|
|
||||||
header += self.format_nbc
|
|
||||||
header += self.format_bit
|
|
||||||
header += self.streamname
|
|
||||||
header += self.framecounter
|
|
||||||
assert (
|
|
||||||
len(header) == HEADER_SIZE + 4
|
|
||||||
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
|
||||||
return header
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class VbanRtPacketHeader:
|
|
||||||
"""Represents the header of a VBAN RT response packet"""
|
|
||||||
|
|
||||||
name = "Voicemeeter-RTP"
|
|
||||||
vban: bytes = "VBAN".encode()
|
|
||||||
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
|
||||||
format_bit: bytes = (0).to_bytes(1, "little")
|
|
||||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
|
||||||
|
|
||||||
@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, f"expected header size {HEADER_SIZE} bytes"
|
|
||||||
return header
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class RequestHeader:
|
|
||||||
"""Represents the header of an REQUEST RT 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")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def sr(self):
|
|
||||||
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def nbc(self):
|
|
||||||
return (self.channel).to_bytes(1, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def streamname(self):
|
|
||||||
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 + 4
|
|
||||||
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
|
||||||
return header
|
|
||||||
266
vban_cmd/packet/headers.py
Normal file
266
vban_cmd/packet/headers.py
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from vban_cmd.enums import NBS
|
||||||
|
from vban_cmd.kinds import KindMapClass
|
||||||
|
|
||||||
|
VBAN_PROTOCOL_TXT = 0x40
|
||||||
|
VBAN_PROTOCOL_SERVICE = 0x60
|
||||||
|
|
||||||
|
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||||
|
VBAN_SERVICE_RTPACKET = 33
|
||||||
|
VBAN_SERVICE_MASK = 0xE0
|
||||||
|
VBAN_PROTOCOL_MASK = 0xE0
|
||||||
|
VBAN_SERVICE_REQUESTREPLY = 0x02
|
||||||
|
VBAN_SERVICE_FNCT_REPLY = 0x02
|
||||||
|
|
||||||
|
MAX_PACKET_SIZE = 1436
|
||||||
|
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanPacket:
|
||||||
|
"""Represents the header of an incoming VBAN data packet"""
|
||||||
|
|
||||||
|
nbs: NBS
|
||||||
|
_kind: KindMapClass
|
||||||
|
_voicemeeterType: bytes
|
||||||
|
_reserved: bytes
|
||||||
|
_buffersize: bytes
|
||||||
|
_voicemeeterVersion: bytes
|
||||||
|
_optionBits: bytes
|
||||||
|
_samplerate: bytes
|
||||||
|
|
||||||
|
@property
|
||||||
|
def voicemeetertype(self) -> str:
|
||||||
|
"""returns voicemeeter type as a string"""
|
||||||
|
return ['', 'basic', 'banana', 'potato'][
|
||||||
|
int.from_bytes(self._voicemeeterType, 'little')
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def voicemeeterversion(self) -> tuple:
|
||||||
|
"""returns voicemeeter version as a tuple"""
|
||||||
|
return tuple(self._voicemeeterVersion[i] for i in range(3, -1, -1))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def samplerate(self) -> int:
|
||||||
|
"""returns samplerate as an int"""
|
||||||
|
return int.from_bytes(self._samplerate, 'little')
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanSubscribeHeader:
|
||||||
|
"""Represents the header of a 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)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_vban_service_header(data: bytes) -> dict:
|
||||||
|
"""Common parsing and validation for VBAN service protocol headers."""
|
||||||
|
if len(data) < HEADER_SIZE:
|
||||||
|
raise ValueError('Data is too short to be a valid VBAN header')
|
||||||
|
|
||||||
|
if data[:4] != b'VBAN':
|
||||||
|
raise ValueError('Invalid VBAN magic bytes')
|
||||||
|
|
||||||
|
format_sr = data[4]
|
||||||
|
format_nbs = data[5]
|
||||||
|
format_nbc = data[6]
|
||||||
|
format_bit = data[7]
|
||||||
|
|
||||||
|
# Verify this is a service protocol packet
|
||||||
|
protocol = format_sr & VBAN_PROTOCOL_MASK
|
||||||
|
if protocol != VBAN_PROTOCOL_SERVICE:
|
||||||
|
raise ValueError(f'Not a service protocol packet: {protocol:02x}')
|
||||||
|
|
||||||
|
# Extract stream name and frame counter
|
||||||
|
name = data[8:24].rstrip(b'\x00').decode('utf-8', errors='ignore')
|
||||||
|
framecounter = int.from_bytes(data[24:28], 'little')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'format_sr': format_sr,
|
||||||
|
'format_nbs': format_nbs,
|
||||||
|
'format_nbc': format_nbc,
|
||||||
|
'format_bit': format_bit,
|
||||||
|
'name': name,
|
||||||
|
'framecounter': framecounter,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanResponseHeader:
|
||||||
|
"""Represents the header of a 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
|
||||||
|
framecounter: 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):
|
||||||
|
"""Parse a VbanResponseHeader from bytes."""
|
||||||
|
parsed = _parse_vban_service_header(data)
|
||||||
|
|
||||||
|
# Validate this is an RTPacket response
|
||||||
|
if parsed['format_nbc'] != VBAN_SERVICE_RTPACKET:
|
||||||
|
raise ValueError(
|
||||||
|
f'Not an RTPacket response packet: {parsed["format_nbc"]:02x}'
|
||||||
|
)
|
||||||
|
|
||||||
|
return cls(**parsed)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanMatrixResponseHeader:
|
||||||
|
"""Represents the header of a matrix response packet"""
|
||||||
|
|
||||||
|
name: str = 'Request Reply'
|
||||||
|
format_sr: int = VBAN_PROTOCOL_SERVICE
|
||||||
|
format_nbs: int = VBAN_SERVICE_FNCT_REPLY
|
||||||
|
format_nbc: int = VBAN_SERVICE_REQUESTREPLY
|
||||||
|
format_bit: int = 0
|
||||||
|
framecounter: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vban(self) -> bytes:
|
||||||
|
return b'VBAN'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def streamname(self) -> bytes:
|
||||||
|
return self.name.encode('ascii')[:16].ljust(16, b'\x00')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_bytes(cls, data: bytes):
|
||||||
|
"""Parse a matrix response packet from bytes."""
|
||||||
|
parsed = _parse_vban_service_header(data)
|
||||||
|
|
||||||
|
# Validate this is a service reply packet
|
||||||
|
if parsed['format_nbs'] != VBAN_SERVICE_FNCT_REPLY:
|
||||||
|
raise ValueError(f'Not a service reply packet: {parsed["format_nbs"]:02x}')
|
||||||
|
|
||||||
|
return cls(**parsed)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def extract_payload(cls, data: bytes) -> str:
|
||||||
|
"""Extract the text payload from a matrix response packet."""
|
||||||
|
if len(data) <= HEADER_SIZE:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
payload_bytes = data[HEADER_SIZE:]
|
||||||
|
return payload_bytes.rstrip(b'\x00').decode('utf-8', errors='ignore')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def parse_response(cls, data: bytes) -> tuple['VbanMatrixResponseHeader', str]:
|
||||||
|
"""Parse a complete matrix response packet returning header and payload."""
|
||||||
|
header = cls.from_bytes(data)
|
||||||
|
payload = cls.extract_payload(data)
|
||||||
|
return header, payload
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanRequestHeader:
|
||||||
|
"""Represents the header of a request packet"""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
bps_index: int
|
||||||
|
channel: int
|
||||||
|
framecounter: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def vban(self) -> bytes:
|
||||||
|
return b'VBAN'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sr(self) -> bytes:
|
||||||
|
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, 'little')
|
||||||
|
|
||||||
|
@property
|
||||||
|
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))
|
||||||
|
|
||||||
|
@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
|
||||||
|
)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def encode_with_payload(
|
||||||
|
cls, name: str, bps_index: int, channel: int, framecounter: int, payload: str
|
||||||
|
) -> bytes:
|
||||||
|
"""Creates the complete packet with header and payload."""
|
||||||
|
return cls.to_bytes(name, bps_index, channel, framecounter) + payload.encode()
|
||||||
288
vban_cmd/packet/nbs0.py
Normal file
288
vban_cmd/packet/nbs0.py
Normal file
@@ -0,0 +1,288 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from vban_cmd.enums import NBS
|
||||||
|
from vban_cmd.kinds import KindMapClass
|
||||||
|
from vban_cmd.util import comp
|
||||||
|
|
||||||
|
from .headers import VbanPacket
|
||||||
|
|
||||||
|
|
||||||
|
class Levels(NamedTuple):
|
||||||
|
strip: tuple[float, ...]
|
||||||
|
bus: tuple[float, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelState:
|
||||||
|
"""Represents the processed state of a single strip or bus channel"""
|
||||||
|
|
||||||
|
def __init__(self, state_bytes: bytes):
|
||||||
|
# Convert 4-byte state to integer once for efficient lookups
|
||||||
|
self._state = int.from_bytes(state_bytes, 'little')
|
||||||
|
|
||||||
|
def get_mode(self, mode_value: int) -> bool:
|
||||||
|
"""Get boolean state for a specific mode"""
|
||||||
|
return (self._state & mode_value) != 0
|
||||||
|
|
||||||
|
def get_mode_int(self, mode_value: int) -> int:
|
||||||
|
"""Get integer state for a specific mode"""
|
||||||
|
return self._state & mode_value
|
||||||
|
|
||||||
|
# Common boolean modes
|
||||||
|
@property
|
||||||
|
def mute(self) -> bool:
|
||||||
|
return (self._state & 0x00000001) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def solo(self) -> bool:
|
||||||
|
return (self._state & 0x00000002) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mono(self) -> bool:
|
||||||
|
return (self._state & 0x00000004) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mc(self) -> bool:
|
||||||
|
return (self._state & 0x00000008) != 0
|
||||||
|
|
||||||
|
# EQ modes
|
||||||
|
@property
|
||||||
|
def eq_on(self) -> bool:
|
||||||
|
return (self._state & 0x00000100) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def eq_ab(self) -> bool:
|
||||||
|
return (self._state & 0x00000800) != 0
|
||||||
|
|
||||||
|
# Bus assignments (strip to bus routing)
|
||||||
|
@property
|
||||||
|
def busa1(self) -> bool:
|
||||||
|
return (self._state & 0x00001000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busa2(self) -> bool:
|
||||||
|
return (self._state & 0x00002000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busa3(self) -> bool:
|
||||||
|
return (self._state & 0x00004000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busa4(self) -> bool:
|
||||||
|
return (self._state & 0x00008000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busb1(self) -> bool:
|
||||||
|
return (self._state & 0x00010000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busb2(self) -> bool:
|
||||||
|
return (self._state & 0x00020000) != 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busb3(self) -> bool:
|
||||||
|
return (self._state & 0x00040000) != 0
|
||||||
|
|
||||||
|
|
||||||
|
class States(NamedTuple):
|
||||||
|
strip: tuple[ChannelState, ...]
|
||||||
|
bus: tuple[ChannelState, ...]
|
||||||
|
|
||||||
|
|
||||||
|
class Labels(NamedTuple):
|
||||||
|
strip: tuple[str, ...]
|
||||||
|
bus: tuple[str, ...]
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class VbanPacketNBS0(VbanPacket):
|
||||||
|
"""Represents the body of a VBAN data packet with ident:0"""
|
||||||
|
|
||||||
|
_inputLeveldB100: bytes
|
||||||
|
_outputLeveldB100: bytes
|
||||||
|
_TransportBit: bytes
|
||||||
|
_stripState: bytes
|
||||||
|
_busState: bytes
|
||||||
|
_stripGaindB100Layer1: bytes
|
||||||
|
_stripGaindB100Layer2: bytes
|
||||||
|
_stripGaindB100Layer3: bytes
|
||||||
|
_stripGaindB100Layer4: bytes
|
||||||
|
_stripGaindB100Layer5: bytes
|
||||||
|
_stripGaindB100Layer6: bytes
|
||||||
|
_stripGaindB100Layer7: bytes
|
||||||
|
_stripGaindB100Layer8: bytes
|
||||||
|
_busGaindB100: bytes
|
||||||
|
_stripLabelUTF8c60: bytes
|
||||||
|
_busLabelUTF8c60: bytes
|
||||||
|
|
||||||
|
@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 pdirty(self, other) -> bool:
|
||||||
|
"""True iff any defined parameter has changed"""
|
||||||
|
|
||||||
|
self_gains = (
|
||||||
|
self._stripGaindB100Layer1
|
||||||
|
+ self._stripGaindB100Layer2
|
||||||
|
+ self._stripGaindB100Layer3
|
||||||
|
+ self._stripGaindB100Layer4
|
||||||
|
+ self._stripGaindB100Layer5
|
||||||
|
+ self._stripGaindB100Layer6
|
||||||
|
+ self._stripGaindB100Layer7
|
||||||
|
+ self._stripGaindB100Layer8
|
||||||
|
)
|
||||||
|
other_gains = (
|
||||||
|
other._stripGaindB100Layer1
|
||||||
|
+ other._stripGaindB100Layer2
|
||||||
|
+ other._stripGaindB100Layer3
|
||||||
|
+ other._stripGaindB100Layer4
|
||||||
|
+ other._stripGaindB100Layer5
|
||||||
|
+ other._stripGaindB100Layer6
|
||||||
|
+ other._stripGaindB100Layer7
|
||||||
|
+ other._stripGaindB100Layer8
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
self._stripState != other._stripState
|
||||||
|
or self._busState != other._busState
|
||||||
|
or self_gains != other_gains
|
||||||
|
or self._busGaindB100 != other._busGaindB100
|
||||||
|
or self._stripLabelUTF8c60 != other._stripLabelUTF8c60
|
||||||
|
or self._busLabelUTF8c60 != other._busLabelUTF8c60
|
||||||
|
)
|
||||||
|
|
||||||
|
def ldirty(self, strip_cache, bus_cache) -> bool:
|
||||||
|
"""True iff any level has changed, ignoring changes when levels are very quiet"""
|
||||||
|
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(self._strip_comp) or any(self._bus_comp)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def strip_levels(self) -> tuple[float, ...]:
|
||||||
|
"""Returns strip levels in dB"""
|
||||||
|
return tuple(
|
||||||
|
round(
|
||||||
|
int.from_bytes(self._inputLeveldB100[i : i + 2], 'little', signed=True)
|
||||||
|
* 0.01,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
for i in range(0, len(self._inputLeveldB100), 2)
|
||||||
|
)[: self._kind.num_strip_levels]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus_levels(self) -> tuple[float, ...]:
|
||||||
|
"""Returns bus levels in dB"""
|
||||||
|
return tuple(
|
||||||
|
round(
|
||||||
|
int.from_bytes(self._outputLeveldB100[i : i + 2], 'little', signed=True)
|
||||||
|
* 0.01,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
for i in range(0, len(self._outputLeveldB100), 2)
|
||||||
|
)[: self._kind.num_bus_levels]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def levels(self) -> Levels:
|
||||||
|
"""Returns strip and bus levels as a namedtuple"""
|
||||||
|
return Levels(strip=self.strip_levels, bus=self.bus_levels)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def states(self) -> States:
|
||||||
|
"""returns States object with processed strip and bus channel states"""
|
||||||
|
return States(
|
||||||
|
strip=tuple(
|
||||||
|
ChannelState(self._stripState[i : i + 4]) for i in range(0, 32, 4)
|
||||||
|
),
|
||||||
|
bus=tuple(ChannelState(self._busState[i : i + 4]) for i in range(0, 32, 4)),
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gainlayers(self) -> tuple:
|
||||||
|
"""returns tuple of all strip gain layers as tuples"""
|
||||||
|
return tuple(
|
||||||
|
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)
|
||||||
|
)
|
||||||
|
for layer in range(1, 9)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def busgain(self) -> tuple:
|
||||||
|
"""returns tuple of bus gains"""
|
||||||
|
return tuple(
|
||||||
|
round(
|
||||||
|
int.from_bytes(self._busGaindB100[i : i + 2], 'little', signed=True)
|
||||||
|
* 0.01,
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
for i in range(0, 16, 2)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def labels(self) -> Labels:
|
||||||
|
"""returns Labels namedtuple of strip and bus labels"""
|
||||||
|
|
||||||
|
def _extract_labels_from_bytes(label_bytes: bytes) -> tuple[str, ...]:
|
||||||
|
"""Extract null-terminated UTF-8 labels from 60-byte chunks"""
|
||||||
|
labels = []
|
||||||
|
for i in range(0, len(label_bytes), 60):
|
||||||
|
chunk = label_bytes[i : i + 60]
|
||||||
|
null_pos = chunk.find(b'\x00')
|
||||||
|
if null_pos == -1:
|
||||||
|
try:
|
||||||
|
label = chunk.decode('utf-8', errors='replace').rstrip('\x00')
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
label = ''
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
label = (
|
||||||
|
chunk[:null_pos].decode('utf-8', errors='replace')
|
||||||
|
if null_pos > 0
|
||||||
|
else ''
|
||||||
|
)
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
label = ''
|
||||||
|
labels.append(label)
|
||||||
|
return tuple(labels)
|
||||||
|
|
||||||
|
return Labels(
|
||||||
|
strip=_extract_labels_from_bytes(self._stripLabelUTF8c60),
|
||||||
|
bus=_extract_labels_from_bytes(self._busLabelUTF8c60),
|
||||||
|
)
|
||||||
357
vban_cmd/packet/nbs1.py
Normal file
357
vban_cmd/packet/nbs1.py
Normal file
@@ -0,0 +1,357 @@
|
|||||||
|
import struct
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
from vban_cmd.enums import NBS
|
||||||
|
from vban_cmd.kinds import KindMapClass
|
||||||
|
|
||||||
|
from .headers import VbanPacket
|
||||||
|
|
||||||
|
VMPARAMSTRIP_SIZE = 174
|
||||||
|
|
||||||
|
|
||||||
|
class Audibility(NamedTuple):
|
||||||
|
knob: float
|
||||||
|
comp: float
|
||||||
|
gate: float
|
||||||
|
denoiser: float
|
||||||
|
|
||||||
|
|
||||||
|
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 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 VbanPacketNBS1(VbanPacket):
|
||||||
|
"""Represents the body of a VBAN data packet with ident: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)
|
||||||
|
),
|
||||||
|
)
|
||||||
138
vban_cmd/recorder.py
Normal file
138
vban_cmd/recorder.py
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
from .error import VBANCMDError
|
||||||
|
from .iremote import IRemote
|
||||||
|
from .meta import action_fn
|
||||||
|
|
||||||
|
|
||||||
|
class Recorder(IRemote):
|
||||||
|
"""
|
||||||
|
Implements the common interface
|
||||||
|
|
||||||
|
Defines concrete implementation for recorder
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make(cls, remote):
|
||||||
|
"""
|
||||||
|
Factory function for recorder class.
|
||||||
|
|
||||||
|
Returns a Recorder class of a kind.
|
||||||
|
"""
|
||||||
|
Recorder_cls = type(
|
||||||
|
f'Recorder{remote.kind}',
|
||||||
|
(cls,),
|
||||||
|
{
|
||||||
|
**{
|
||||||
|
param: action_fn(param)
|
||||||
|
for param in [
|
||||||
|
'play',
|
||||||
|
'stop',
|
||||||
|
'pause',
|
||||||
|
'replay',
|
||||||
|
'record',
|
||||||
|
'ff',
|
||||||
|
'rew',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return Recorder_cls(remote)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f'{type(self).__name__}'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return 'recorder'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def samplerate(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@samplerate.setter
|
||||||
|
def samplerate(self, val: int):
|
||||||
|
opts = (22050, 24000, 32000, 44100, 48000, 88200, 96000, 176400, 192000)
|
||||||
|
if val not in opts:
|
||||||
|
self.logger.warning(f'samplerate got: {val} but expected a value in {opts}')
|
||||||
|
self.setter('samplerate', val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bitresolution(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@bitresolution.setter
|
||||||
|
def bitresolution(self, val: int):
|
||||||
|
opts = (8, 16, 24, 32)
|
||||||
|
if val not in opts:
|
||||||
|
self.logger.warning(
|
||||||
|
f'bitresolution got: {val} but expected a value in {opts}'
|
||||||
|
)
|
||||||
|
self.setter('bitresolution', 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 kbps(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
@kbps.setter
|
||||||
|
def kbps(self, val: int):
|
||||||
|
opts = (32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320)
|
||||||
|
if val not in opts:
|
||||||
|
self.logger.warning(f'kbps got: {val} but expected a value in {opts}')
|
||||||
|
self.setter('kbps', val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gain(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@gain.setter
|
||||||
|
def gain(self, val: float):
|
||||||
|
self.setter('gain', val)
|
||||||
|
|
||||||
|
def load(self, file: os.PathLike):
|
||||||
|
try:
|
||||||
|
# Convert to string, use forward slashes, and wrap in quotes for spaces
|
||||||
|
file_path = f'"{os.fspath(file).replace(chr(92), "/")}"'
|
||||||
|
self.setter('load', file_path)
|
||||||
|
except UnicodeError:
|
||||||
|
raise VBANCMDError('File full directory must be a raw string')
|
||||||
|
|
||||||
|
def goto(self, time_str):
|
||||||
|
def get_sec():
|
||||||
|
"""Get seconds from time string"""
|
||||||
|
h, m, s = time_str.split(':')
|
||||||
|
return int(h) * 3600 + int(m) * 60 + int(s)
|
||||||
|
|
||||||
|
time_str = str(time_str) # coerce the type
|
||||||
|
if (
|
||||||
|
re.match(
|
||||||
|
r'^(?:[01]\d|2[0123]):(?:[012345]\d):(?:[012345]\d)$',
|
||||||
|
time_str,
|
||||||
|
)
|
||||||
|
is not None
|
||||||
|
):
|
||||||
|
self.setter('goto', get_sec())
|
||||||
|
else:
|
||||||
|
self.logger.warning(
|
||||||
|
"goto expects a string that matches the format 'hh:mm:ss'"
|
||||||
|
)
|
||||||
|
|
||||||
|
def filetype(self, val: str):
|
||||||
|
opts = {'wav': 1, 'aiff': 2, 'bwf': 3, 'mp3': 100}
|
||||||
|
try:
|
||||||
|
self.setter('filetype', opts[val.lower()])
|
||||||
|
except KeyError:
|
||||||
|
self.logger.warning(
|
||||||
|
f'filetype got: {val} but expected a value in {list(opts.keys())}'
|
||||||
|
)
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
|
import abc
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
|
from . import kinds
|
||||||
|
from .enums import NBS
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .kinds import kinds_all
|
from .meta import (
|
||||||
from .meta import channel_bool_prop, channel_label_prop, strip_output_prop
|
channel_bool_prop,
|
||||||
|
channel_label_prop,
|
||||||
|
send_prop,
|
||||||
|
strip_output_prop,
|
||||||
|
xy_prop,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Strip(IRemote):
|
class Strip(IRemote):
|
||||||
@@ -14,13 +21,13 @@ class Strip(IRemote):
|
|||||||
Defines concrete implementation for strip
|
Defines concrete implementation for strip
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}]"
|
return f'strip[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def limit(self) -> int:
|
def limit(self) -> int:
|
||||||
@@ -28,212 +35,268 @@ class Strip(IRemote):
|
|||||||
|
|
||||||
@limit.setter
|
@limit.setter
|
||||||
def limit(self, val: int):
|
def limit(self, val: int):
|
||||||
self.setter("limit", val)
|
self.setter('limit', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gain(self) -> float:
|
def gain(self) -> float:
|
||||||
val = self.getter("gain")
|
val = self.getter('gain')
|
||||||
if val is None:
|
if val is None:
|
||||||
val = self.gainlayer[0].gain
|
val = max(layer.gain for layer in self.gainlayer)
|
||||||
return round(val, 1)
|
return round(val, 1)
|
||||||
|
|
||||||
@gain.setter
|
@gain.setter
|
||||||
def gain(self, val: float):
|
def gain(self, val: float):
|
||||||
self.setter("gain", val)
|
self.setter('gain', val)
|
||||||
|
|
||||||
def fadeto(self, target: float, time_: int):
|
def fadeto(self, target: float, time_: int):
|
||||||
self.setter("FadeTo", f"({target}, {time_})")
|
self.setter('FadeTo', f'({target}, {time_})')
|
||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|
||||||
def fadeby(self, change: float, time_: int):
|
def fadeby(self, change: float, time_: int):
|
||||||
self.setter("FadeBy", f"({change}, {time_})")
|
self.setter('FadeBy', f'({change}, {time_})')
|
||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|
||||||
|
|
||||||
class PhysicalStrip(Strip):
|
class PhysicalStrip(Strip):
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, remote, index):
|
def make(cls, remote, index, is_phys):
|
||||||
|
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||||
return type(
|
return type(
|
||||||
f"PhysicalStrip{remote.kind}",
|
f'PhysicalStrip{remote.kind}',
|
||||||
(cls,),
|
(cls, EFFECTS_cls),
|
||||||
{
|
{
|
||||||
"comp": StripComp(remote, index),
|
'comp': StripComp(remote, index),
|
||||||
"gate": StripGate(remote, index),
|
'gate': StripGate(remote, index),
|
||||||
"denoiser": StripDenoiser(remote, index),
|
'denoiser': StripDenoiser(remote, index),
|
||||||
"eq": StripEQ(remote, index),
|
'eq': StripEQ.make(remote, index),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f'{type(self).__name__}{self.index}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device(self):
|
def audibility(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].audibility.knob
|
||||||
|
|
||||||
@property
|
@audibility.setter
|
||||||
def sr(self):
|
def audibility(self, val: float):
|
||||||
return
|
self.setter('audibility', val)
|
||||||
|
|
||||||
|
|
||||||
class StripComp(IRemote):
|
class StripComp(IRemote):
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}].comp"
|
return f'strip[{self.index}].comp'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def knob(self) -> float:
|
def knob(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].audibility.comp
|
||||||
|
|
||||||
@knob.setter
|
@knob.setter
|
||||||
def knob(self, val: float):
|
def knob(self, val: float):
|
||||||
self.setter("", val)
|
self.setter('', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gainin(self) -> float:
|
def gainin(self) -> float:
|
||||||
return
|
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
|
@gainin.setter
|
||||||
def gainin(self, val: float):
|
def gainin(self, val: float):
|
||||||
self.setter("GainIn", val)
|
self.setter('GainIn', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ratio(self) -> float:
|
def ratio(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].compressor.ratio
|
||||||
|
|
||||||
@ratio.setter
|
@ratio.setter
|
||||||
def ratio(self, val: float):
|
def ratio(self, val: float):
|
||||||
self.setter("Ratio", val)
|
self.setter('Ratio', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def threshold(self) -> float:
|
def threshold(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].compressor.threshold
|
||||||
|
|
||||||
@threshold.setter
|
@threshold.setter
|
||||||
def threshold(self, val: float):
|
def threshold(self, val: float):
|
||||||
self.setter("Threshold", val)
|
self.setter('Threshold', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack(self) -> float:
|
def attack(self) -> float:
|
||||||
return
|
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
|
@attack.setter
|
||||||
def attack(self, val: float):
|
def attack(self, val: float):
|
||||||
self.setter("Attack", val)
|
self.setter('Attack', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def release(self) -> float:
|
def release(self) -> float:
|
||||||
return
|
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
|
@release.setter
|
||||||
def release(self, val: float):
|
def release(self, val: float):
|
||||||
self.setter("Release", val)
|
self.setter('Release', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def knee(self) -> float:
|
def knee(self) -> float:
|
||||||
return
|
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
|
@knee.setter
|
||||||
def knee(self, val: float):
|
def knee(self, val: float):
|
||||||
self.setter("Knee", val)
|
self.setter('Knee', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gainout(self) -> float:
|
def gainout(self) -> float:
|
||||||
return
|
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
|
@gainout.setter
|
||||||
def gainout(self, val: float):
|
def gainout(self, val: float):
|
||||||
self.setter("GainOut", val)
|
self.setter('GainOut', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def makeup(self) -> bool:
|
def makeup(self) -> bool:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return False
|
||||||
|
return bool(self.public_packets[NBS.one].strips[self.index].compressor.makeup)
|
||||||
|
|
||||||
@makeup.setter
|
@makeup.setter
|
||||||
def makeup(self, val: bool):
|
def makeup(self, val: bool):
|
||||||
self.setter("makeup", 1 if val else 0)
|
self.setter('makeup', 1 if val else 0)
|
||||||
|
|
||||||
|
|
||||||
class StripGate(IRemote):
|
class StripGate(IRemote):
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}].gate"
|
return f'strip[{self.index}].gate'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def knob(self) -> float:
|
def knob(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].audibility.gate
|
||||||
|
|
||||||
@knob.setter
|
@knob.setter
|
||||||
def knob(self, val: float):
|
def knob(self, val: float):
|
||||||
self.setter("", val)
|
self.setter('', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def threshold(self) -> float:
|
def threshold(self) -> float:
|
||||||
return
|
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
|
@threshold.setter
|
||||||
def threshold(self, val: float):
|
def threshold(self, val: float):
|
||||||
self.setter("Threshold", val)
|
self.setter('Threshold', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def damping(self) -> float:
|
def damping(self) -> float:
|
||||||
return
|
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
|
@damping.setter
|
||||||
def damping(self, val: float):
|
def damping(self, val: float):
|
||||||
self.setter("Damping", val)
|
self.setter('Damping', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bpsidechain(self) -> int:
|
def bpsidechain(self) -> int:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].gate.bp_sidechain
|
||||||
|
|
||||||
@bpsidechain.setter
|
@bpsidechain.setter
|
||||||
def bpsidechain(self, val: int):
|
def bpsidechain(self, val: int):
|
||||||
self.setter("BPSidechain", val)
|
self.setter('BPSidechain', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def attack(self) -> float:
|
def attack(self) -> float:
|
||||||
return
|
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
|
@attack.setter
|
||||||
def attack(self, val: float):
|
def attack(self, val: float):
|
||||||
self.setter("Attack", val)
|
self.setter('Attack', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hold(self) -> float:
|
def hold(self) -> float:
|
||||||
return
|
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
|
@hold.setter
|
||||||
def hold(self, val: float):
|
def hold(self, val: float):
|
||||||
self.setter("Hold", val)
|
self.setter('Hold', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def release(self) -> float:
|
def release(self) -> float:
|
||||||
return
|
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
|
@release.setter
|
||||||
def release(self, val: float):
|
def release(self, val: float):
|
||||||
self.setter("Release", val)
|
self.setter('Release', val)
|
||||||
|
|
||||||
|
|
||||||
class StripDenoiser(IRemote):
|
class StripDenoiser(IRemote):
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}].denoiser"
|
return f'strip[{self.index}].denoiser'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def knob(self) -> float:
|
def knob(self) -> float:
|
||||||
return
|
if self.public_packets[NBS.one] is None:
|
||||||
|
return 0.0
|
||||||
|
return self.public_packets[NBS.one].strips[self.index].audibility.denoiser
|
||||||
|
|
||||||
@knob.setter
|
@knob.setter
|
||||||
def knob(self, val: float):
|
def knob(self, val: float):
|
||||||
self.setter("", val)
|
self.setter('', val)
|
||||||
|
|
||||||
|
|
||||||
class StripEQ(IRemote):
|
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
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}].eq"
|
return f'strip[{self.index}].eq'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def on(self):
|
def on(self):
|
||||||
@@ -241,7 +304,7 @@ class StripEQ(IRemote):
|
|||||||
|
|
||||||
@on.setter
|
@on.setter
|
||||||
def on(self, val: bool):
|
def on(self, val: bool):
|
||||||
self.setter("on", 1 if val else 0)
|
self.setter('on', 1 if val else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ab(self):
|
def ab(self):
|
||||||
@@ -249,30 +312,214 @@ class StripEQ(IRemote):
|
|||||||
|
|
||||||
@ab.setter
|
@ab.setter
|
||||||
def ab(self, val: bool):
|
def ab(self, val: bool):
|
||||||
self.setter("ab", 1 if val else 0)
|
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):
|
class VirtualStrip(Strip):
|
||||||
def __str__(self):
|
@classmethod
|
||||||
return f"{type(self).__name__}{self.index}"
|
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
|
mono = mc
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def k(self) -> int:
|
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
|
@k.setter
|
||||||
def k(self, val: int):
|
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):
|
def appgain(self, name: str, gain: float):
|
||||||
self.setter("AppGain", f'("{name}", {gain})')
|
self.setter('AppGain', f'("{name}", {gain})')
|
||||||
|
|
||||||
def appmute(self, name: str, mute: bool = None):
|
def appmute(self, name: str, mute: bool = None):
|
||||||
self.setter("AppMute", f'("{name}", {1 if mute else 0})')
|
self.setter('AppMute', f'("{name}", {1 if mute else 0})')
|
||||||
|
|
||||||
|
|
||||||
class StripLevel(IRemote):
|
class StripLevel(IRemote):
|
||||||
@@ -293,26 +540,15 @@ class StripLevel(IRemote):
|
|||||||
def getter(self):
|
def getter(self):
|
||||||
"""Returns a tuple of level values for the channel."""
|
"""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:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return self._remote.cache['strip_level'][self.range[0] : self.range[-1]]
|
||||||
fget(i)
|
return self.public_packets[NBS.zero].levels.strip[
|
||||||
for i in self._remote.cache["strip_level"][
|
|
||||||
self.range[0] : self.range[-1]
|
self.range[0] : self.range[-1]
|
||||||
]
|
]
|
||||||
)
|
|
||||||
return tuple(
|
|
||||||
fget(i)
|
|
||||||
for i in self._remote._get_levels(self.public_packet)[0][
|
|
||||||
self.range[0] : self.range[-1]
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}]"
|
return f'strip[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def prefader(self) -> tuple:
|
def prefader(self) -> tuple:
|
||||||
@@ -345,31 +581,28 @@ class GainLayer(IRemote):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"Strip[{self.index}]"
|
return f'strip[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def gain(self) -> float:
|
def gain(self) -> float:
|
||||||
def fget():
|
val = self.getter(f'GainLayer[{self._i}]')
|
||||||
val = getattr(self.public_packet, f"stripgainlayer{self._i+1}")[self.index]
|
if val:
|
||||||
if 0 <= val <= 1200:
|
return round(val, 2)
|
||||||
return val * 0.01
|
else:
|
||||||
return (((1 << 16) - 1) - val) * -0.01
|
return self.public_packets[NBS.zero].gainlayers[self._i][self.index]
|
||||||
|
|
||||||
val = self.getter(f"GainLayer[{self._i}]")
|
|
||||||
return round(val if val else fget(), 1)
|
|
||||||
|
|
||||||
@gain.setter
|
@gain.setter
|
||||||
def gain(self, val: float):
|
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):
|
def _make_gainlayer_mixin(remote, index):
|
||||||
"""Creates a GainLayer mixin"""
|
"""Creates a GainLayer mixin"""
|
||||||
return type(
|
return type(
|
||||||
f"GainlayerMixin",
|
'GainlayerMixin',
|
||||||
(),
|
(),
|
||||||
{
|
{
|
||||||
"gainlayer": tuple(
|
'gainlayer': tuple(
|
||||||
GainLayer(remote, index, i) for i in range(remote.kind.num_bus)
|
GainLayer(remote, index, i) for i in range(remote.kind.num_bus)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -379,24 +612,78 @@ def _make_gainlayer_mixin(remote, index):
|
|||||||
def _make_channelout_mixin(kind):
|
def _make_channelout_mixin(kind):
|
||||||
"""Creates a channel out property mixin"""
|
"""Creates a channel out property mixin"""
|
||||||
return type(
|
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 = {
|
_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]:
|
def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip]:
|
||||||
"""
|
"""
|
||||||
Factory method for strips
|
Factory method for strips
|
||||||
@@ -405,17 +692,21 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
|||||||
|
|
||||||
Returns a physical or virtual strip subclass
|
Returns a physical or virtual strip subclass
|
||||||
"""
|
"""
|
||||||
STRIP_cls = PhysicalStrip.make(remote, i) 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]
|
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||||
|
|
||||||
return type(
|
return type(
|
||||||
f"{STRIP_cls.__name__}{remote.kind}",
|
f'{STRIP_cls.__name__}{remote.kind}',
|
||||||
(STRIP_cls, CHANNELOUTMIXIN_cls, GAINLAYERMIXIN_cls),
|
(STRIP_cls, CHANNELOUTMIXIN_cls, GAINLAYERMIXIN_cls),
|
||||||
{
|
{
|
||||||
"levels": StripLevel(remote, i),
|
'levels': StripLevel(remote, i),
|
||||||
**{param: channel_bool_prop(param) for param in ["mono", "solo", "mute"]},
|
**{param: channel_bool_prop(param) for param in ['mono', 'solo', 'mute']},
|
||||||
"label": channel_label_prop(),
|
'label': channel_label_prop(),
|
||||||
},
|
},
|
||||||
)(remote, i)
|
)(remote, i)
|
||||||
|
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ class Subject:
|
|||||||
"""run callbacks on update"""
|
"""run callbacks on update"""
|
||||||
|
|
||||||
for o in self._observers:
|
for o in self._observers:
|
||||||
if hasattr(o, "on_update"):
|
if hasattr(o, 'on_update'):
|
||||||
o.on_update(event)
|
o.on_update(event)
|
||||||
else:
|
else:
|
||||||
if o.__name__ == f"on_{event}":
|
if o.__name__ == f'on_{event}':
|
||||||
o()
|
o()
|
||||||
|
|
||||||
def add(self, observer):
|
def add(self, observer):
|
||||||
@@ -34,15 +34,15 @@ class Subject:
|
|||||||
for o in iterator:
|
for o in iterator:
|
||||||
if o not in self._observers:
|
if o not in self._observers:
|
||||||
self._observers.append(o)
|
self._observers.append(o)
|
||||||
self.logger.info(f"{o} added to event observers")
|
self.logger.info(f'{o} added to event observers')
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"Failed to add {o} to event observers")
|
self.logger.error(f'Failed to add {o} to event observers')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
if observer not in self._observers:
|
if observer not in self._observers:
|
||||||
self._observers.append(observer)
|
self._observers.append(observer)
|
||||||
self.logger.info(f"{observer} added to event observers")
|
self.logger.info(f'{observer} added to event observers')
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"Failed to add {observer} to event observers")
|
self.logger.error(f'Failed to add {observer} to event observers')
|
||||||
|
|
||||||
register = add
|
register = add
|
||||||
|
|
||||||
@@ -54,15 +54,15 @@ class Subject:
|
|||||||
for o in iterator:
|
for o in iterator:
|
||||||
try:
|
try:
|
||||||
self._observers.remove(o)
|
self._observers.remove(o)
|
||||||
self.logger.info(f"{o} removed from event observers")
|
self.logger.info(f'{o} removed from event observers')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.logger.error(f"Failed to remove {o} from event observers")
|
self.logger.error(f'Failed to remove {o} from event observers')
|
||||||
except TypeError:
|
except TypeError:
|
||||||
try:
|
try:
|
||||||
self._observers.remove(observer)
|
self._observers.remove(observer)
|
||||||
self.logger.info(f"{observer} removed from event observers")
|
self.logger.info(f'{observer} removed from event observers')
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.logger.error(f"Failed to remove {observer} from event observers")
|
self.logger.error(f'Failed to remove {observer} from event observers')
|
||||||
|
|
||||||
deregister = remove
|
deregister = remove
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
from enum import IntEnum
|
|
||||||
from typing import Iterator
|
from typing import Iterator
|
||||||
|
|
||||||
|
|
||||||
@@ -7,9 +6,22 @@ def cache_bool(func, param):
|
|||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
self, *rem = args
|
self, *rem = args
|
||||||
cmd = f"{self.identifier}.{param}"
|
if self._cmd(param) in self._remote.cache:
|
||||||
if cmd in self._remote.cache:
|
return self._remote.cache.pop(self._cmd(param)) == 1
|
||||||
return self._remote.cache.pop(cmd) == 1
|
if self._remote.sync:
|
||||||
|
self._remote.clear_dirty()
|
||||||
|
return func(*args, **kwargs)
|
||||||
|
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def cache_int(func, param):
|
||||||
|
"""Check cache for an int prop"""
|
||||||
|
|
||||||
|
def wrapper(*args, **kwargs):
|
||||||
|
self, *rem = args
|
||||||
|
if self._cmd(param) in self._remote.cache:
|
||||||
|
return self._remote.cache.pop(self._cmd(param))
|
||||||
if self._remote.sync:
|
if self._remote.sync:
|
||||||
self._remote.clear_dirty()
|
self._remote.clear_dirty()
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
@@ -22,9 +34,22 @@ def cache_string(func, param):
|
|||||||
|
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
self, *rem = args
|
self, *rem = args
|
||||||
cmd = f"{self.identifier}.{param}"
|
if self._cmd(param) in self._remote.cache:
|
||||||
if cmd in self._remote.cache:
|
return self._remote.cache.pop(self._cmd(param)).strip('"')
|
||||||
return self._remote.cache.pop(cmd)
|
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:
|
if self._remote.sync:
|
||||||
self._remote.clear_dirty()
|
self._remote.clear_dirty()
|
||||||
return func(*args, **kwargs)
|
return func(*args, **kwargs)
|
||||||
@@ -38,39 +63,20 @@ def depth(d):
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def script(func):
|
|
||||||
"""Convert dictionary to script"""
|
|
||||||
|
|
||||||
def wrapper(*args):
|
|
||||||
remote, script = args
|
|
||||||
if isinstance(script, dict):
|
|
||||||
params = ""
|
|
||||||
for key, val in script.items():
|
|
||||||
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}"
|
|
||||||
for k, v in val.items()
|
|
||||||
)
|
|
||||||
params += ";"
|
|
||||||
script = params
|
|
||||||
return func(remote, script)
|
|
||||||
|
|
||||||
return wrapper
|
|
||||||
|
|
||||||
|
|
||||||
def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
||||||
"""
|
"""
|
||||||
Generator function, accepts two tuples.
|
Generator function, accepts two tuples of dB values.
|
||||||
|
|
||||||
Evaluates equality of each member in both tuples.
|
Evaluates equality of each member in both tuples.
|
||||||
|
Only ignores changes when levels are very quiet (below -72 dB).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
for a, b in zip(t0, t1):
|
for a, b in zip(t0, t1):
|
||||||
if ((1 << 16) - 1) - b <= 7200:
|
# If both values are very quiet (below -72dB), ignore small changes
|
||||||
yield a == b
|
if a <= -72.0 and b <= -72.0:
|
||||||
|
yield a == b # Both quiet, check if they're equal
|
||||||
else:
|
else:
|
||||||
yield True
|
yield a != b # At least one has significant level, detect changes
|
||||||
|
|
||||||
|
|
||||||
def deep_merge(dict1, dict2):
|
def deep_merge(dict1, dict2):
|
||||||
@@ -87,4 +93,9 @@ def deep_merge(dict1, dict2):
|
|||||||
yield k, dict2[k]
|
yield k, dict2[k]
|
||||||
|
|
||||||
|
|
||||||
Socket = IntEnum("Socket", "register request response", start=0)
|
def bump_framecounter(framecounter: int) -> int:
|
||||||
|
"""Increment framecounter with rollover at 0xFFFFFFFF."""
|
||||||
|
if framecounter > 0xFFFFFFFF:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return framecounter + 1
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
from abc import abstractmethod
|
import abc
|
||||||
|
|
||||||
|
from . import kinds
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .kinds import kinds_all
|
|
||||||
|
|
||||||
|
|
||||||
class VbanStream(IRemote):
|
class VbanStream(IRemote):
|
||||||
@@ -11,13 +11,13 @@ class VbanStream(IRemote):
|
|||||||
Defines concrete implementation for vban stream
|
Defines concrete implementation for vban stream
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self) -> str:
|
def identifier(self) -> str:
|
||||||
return f"vban.{self.direction}stream[{self.index}]"
|
return f'vban.{self.direction}stream[{self.index}]'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def on(self) -> bool:
|
def on(self) -> bool:
|
||||||
@@ -25,7 +25,7 @@ class VbanStream(IRemote):
|
|||||||
|
|
||||||
@on.setter
|
@on.setter
|
||||||
def on(self, val: bool):
|
def on(self, val: bool):
|
||||||
self.setter("on", 1 if val else 0)
|
self.setter('on', 1 if val else 0)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self) -> str:
|
def name(self) -> str:
|
||||||
@@ -33,7 +33,7 @@ class VbanStream(IRemote):
|
|||||||
|
|
||||||
@name.setter
|
@name.setter
|
||||||
def name(self, val: str):
|
def name(self, val: str):
|
||||||
self.setter("name", val)
|
self.setter('name', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def ip(self) -> str:
|
def ip(self) -> str:
|
||||||
@@ -41,7 +41,7 @@ class VbanStream(IRemote):
|
|||||||
|
|
||||||
@ip.setter
|
@ip.setter
|
||||||
def ip(self, val: str):
|
def ip(self, val: str):
|
||||||
self.setter("ip", val)
|
self.setter('ip', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def port(self) -> int:
|
def port(self) -> int:
|
||||||
@@ -51,9 +51,9 @@ class VbanStream(IRemote):
|
|||||||
def port(self, val: int):
|
def port(self, val: int):
|
||||||
if not 1024 <= val <= 65535:
|
if not 1024 <= val <= 65535:
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
f"port got: {val} but expected a value from 1024 to 65535"
|
f'port got: {val} but expected a value from 1024 to 65535'
|
||||||
)
|
)
|
||||||
self.setter("port", val)
|
self.setter('port', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sr(self) -> int:
|
def sr(self) -> int:
|
||||||
@@ -63,8 +63,8 @@ class VbanStream(IRemote):
|
|||||||
def sr(self, val: int):
|
def sr(self, val: int):
|
||||||
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||||
if val not in opts:
|
if val not in opts:
|
||||||
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
self.logger.warning(f'sr got: {val} but expected a value in {opts}')
|
||||||
self.setter("sr", val)
|
self.setter('sr', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def channel(self) -> int:
|
def channel(self) -> int:
|
||||||
@@ -73,8 +73,8 @@ class VbanStream(IRemote):
|
|||||||
@channel.setter
|
@channel.setter
|
||||||
def channel(self, val: int):
|
def channel(self, val: int):
|
||||||
if not 1 <= val <= 8:
|
if not 1 <= val <= 8:
|
||||||
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
|
self.logger.warning(f'channel got: {val} but expected a value from 1 to 8')
|
||||||
self.setter("channel", val)
|
self.setter('channel', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def bit(self) -> int:
|
def bit(self) -> int:
|
||||||
@@ -83,8 +83,8 @@ class VbanStream(IRemote):
|
|||||||
@bit.setter
|
@bit.setter
|
||||||
def bit(self, val: int):
|
def bit(self, val: int):
|
||||||
if val not in (16, 24):
|
if val not in (16, 24):
|
||||||
self.logger.warning(f"bit got: {val} but expected value 16 or 24")
|
self.logger.warning(f'bit got: {val} but expected value 16 or 24')
|
||||||
self.setter("bit", 1 if (val == 16) else 2)
|
self.setter('bit', 1 if (val == 16) else 2)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def quality(self) -> int:
|
def quality(self) -> int:
|
||||||
@@ -93,8 +93,8 @@ class VbanStream(IRemote):
|
|||||||
@quality.setter
|
@quality.setter
|
||||||
def quality(self, val: int):
|
def quality(self, val: int):
|
||||||
if not 0 <= val <= 4:
|
if not 0 <= val <= 4:
|
||||||
self.logger.warning(f"quality got: {val} but expected a value from 0 to 4")
|
self.logger.warning(f'quality got: {val} but expected a value from 0 to 4')
|
||||||
self.setter("quality", val)
|
self.setter('quality', val)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def route(self) -> int:
|
def route(self) -> int:
|
||||||
@@ -103,8 +103,8 @@ class VbanStream(IRemote):
|
|||||||
@route.setter
|
@route.setter
|
||||||
def route(self, val: int):
|
def route(self, val: int):
|
||||||
if not 0 <= val <= 8:
|
if not 0 <= val <= 8:
|
||||||
self.logger.warning(f"route got: {val} but expected a value from 0 to 8")
|
self.logger.warning(f'route got: {val} but expected a value from 0 to 8')
|
||||||
self.setter("route", val)
|
self.setter('route', val)
|
||||||
|
|
||||||
|
|
||||||
class VbanInstream(VbanStream):
|
class VbanInstream(VbanStream):
|
||||||
@@ -115,11 +115,11 @@ class VbanInstream(VbanStream):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
return f'{type(self).__name__}{self._remote.kind}{self.index}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def direction(self) -> str:
|
def direction(self) -> str:
|
||||||
return "in"
|
return 'in'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sr(self) -> int:
|
def sr(self) -> int:
|
||||||
@@ -154,11 +154,11 @@ class VbanOutstream(VbanStream):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
return f'{type(self).__name__}{self._remote.kind}{self.index}'
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def direction(self) -> str:
|
def direction(self) -> str:
|
||||||
return "out"
|
return 'out'
|
||||||
|
|
||||||
|
|
||||||
class VbanAudioOutstream(VbanOutstream):
|
class VbanAudioOutstream(VbanOutstream):
|
||||||
@@ -172,37 +172,29 @@ class VbanMidiOutstream(VbanOutstream):
|
|||||||
def _make_stream_pair(remote, kind):
|
def _make_stream_pair(remote, kind):
|
||||||
num_instream, num_outstream, num_midi, num_text = kind.vban
|
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||||
|
|
||||||
def _generate_streams(i, dir):
|
def _make_cls(i, direction):
|
||||||
"""generator function for creating instream/outstream tuples"""
|
match direction:
|
||||||
if dir == "in":
|
case 'in':
|
||||||
if i < num_instream:
|
if i < num_instream:
|
||||||
yield VbanAudioInstream
|
return VbanAudioInstream(remote, i)
|
||||||
elif i < num_instream + num_midi:
|
elif i < num_instream + num_midi:
|
||||||
yield VbanMidiInstream
|
return VbanMidiInstream(remote, i)
|
||||||
else:
|
|
||||||
yield VbanTextInstream
|
|
||||||
else:
|
else:
|
||||||
|
return VbanTextInstream(remote, i)
|
||||||
|
case 'out':
|
||||||
if i < num_outstream:
|
if i < num_outstream:
|
||||||
yield VbanAudioOutstream
|
return VbanAudioOutstream(remote, i)
|
||||||
else:
|
else:
|
||||||
yield VbanMidiOutstream
|
return VbanMidiOutstream(remote, i)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
tuple(
|
tuple(_make_cls(i, 'in') for i in range(num_instream + num_midi + num_text)),
|
||||||
cls(remote, i)
|
tuple(_make_cls(i, 'out') for i in range(num_outstream + num_midi)),
|
||||||
for i in range(num_instream + num_midi + num_text)
|
|
||||||
for cls in _generate_streams(i, "in")
|
|
||||||
),
|
|
||||||
tuple(
|
|
||||||
cls(remote, i)
|
|
||||||
for i in range(num_outstream + num_midi)
|
|
||||||
for cls in _generate_streams(i, "out")
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _make_stream_pairs(remote):
|
def _make_stream_pairs(remote):
|
||||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds.all}
|
||||||
|
|
||||||
|
|
||||||
class Vban:
|
class Vban:
|
||||||
@@ -220,7 +212,7 @@ class Vban:
|
|||||||
"""if VBAN disabled there can be no communication with it"""
|
"""if VBAN disabled there can be no communication with it"""
|
||||||
|
|
||||||
def disable(self):
|
def disable(self):
|
||||||
self.remote._set_rt("vban.Enable", 0)
|
self.remote._set_rt('vban.Enable', 0)
|
||||||
|
|
||||||
|
|
||||||
def vban_factory(remote) -> Vban:
|
def vban_factory(remote) -> Vban:
|
||||||
@@ -230,7 +222,7 @@ def vban_factory(remote) -> Vban:
|
|||||||
Returns a class that represents the VBAN module.
|
Returns a class that represents the VBAN module.
|
||||||
"""
|
"""
|
||||||
VBAN_cls = Vban
|
VBAN_cls = Vban
|
||||||
return type(f"{VBAN_cls.__name__}", (VBAN_cls,), {})(remote)
|
return type(f'{VBAN_cls.__name__}', (VBAN_cls,), {})(remote)
|
||||||
|
|
||||||
|
|
||||||
def request_vban_obj(remote) -> Vban:
|
def request_vban_obj(remote) -> Vban:
|
||||||
|
|||||||
@@ -1,24 +1,25 @@
|
|||||||
|
import abc
|
||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Iterable, Union
|
from typing import Union
|
||||||
|
|
||||||
|
from .enums import NBS
|
||||||
from .error import VBANCMDError
|
from .error import VBANCMDError
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .packet import RequestHeader
|
from .packet.headers import VbanMatrixResponseHeader, VbanRequestHeader
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
from .util import Socket, deep_merge, script
|
from .util import bump_framecounter, deep_merge
|
||||||
from .worker import Producer, Subscriber, Updater
|
from .worker import Producer, Subscriber, Updater
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VbanCmd(metaclass=ABCMeta):
|
class VbanCmd(abc.ABC):
|
||||||
"""Base class responsible for communicating with the VBAN RT Packet Service"""
|
"""Abstract Base Class for Voicemeeter VBAN Command Interfaces"""
|
||||||
|
|
||||||
DELAY = 0.001
|
DELAY = 0.001
|
||||||
# fmt: off
|
# fmt: off
|
||||||
@@ -31,27 +32,24 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.event = Event({k: kwargs.pop(k) for k in ("pdirty", "ldirty")})
|
self.event = Event({k: kwargs.pop(k) for k in ('pdirty', 'ldirty')})
|
||||||
if not kwargs["ip"]:
|
if not kwargs['ip']:
|
||||||
kwargs |= self._conn_from_toml()
|
kwargs |= self._conn_from_toml()
|
||||||
for attr, val in kwargs.items():
|
for attr, val in kwargs.items():
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
|
|
||||||
self.packet_request = RequestHeader(
|
self._framecounter = 0
|
||||||
name=self.streamname,
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
bps_index=self.BPS_OPTS.index(self.bps),
|
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
channel=self.channel,
|
|
||||||
)
|
|
||||||
self.socks = tuple(
|
|
||||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
|
||||||
)
|
|
||||||
self.subject = self.observer = Subject()
|
self.subject = self.observer = Subject()
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self._pdirty = False
|
self._pdirty = False
|
||||||
self._ldirty = False
|
self._ldirty = False
|
||||||
self._script = str()
|
self._script = str()
|
||||||
|
self.stop_event = None
|
||||||
|
self.producer = None
|
||||||
|
|
||||||
@abstractmethod
|
@abc.abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Ensure subclasses override str magic method"""
|
"""Ensure subclasses override str magic method"""
|
||||||
pass
|
pass
|
||||||
@@ -60,35 +58,36 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
try:
|
try:
|
||||||
import tomllib
|
import tomllib
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
import tomli as tomllib
|
import tomli as tomllib # type: ignore[import]
|
||||||
|
|
||||||
def get_filepath():
|
def get_filepath():
|
||||||
filepaths = [
|
for pn in (
|
||||||
Path.cwd() / "vban.toml",
|
Path.cwd() / 'vban.toml',
|
||||||
Path.cwd() / "configs" / "vban.toml",
|
Path.cwd() / 'configs' / 'vban.toml',
|
||||||
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
Path.home() / '.config' / 'vban-cmd' / 'vban.toml',
|
||||||
Path.home() / "Documents" / "Voicemeeter" / "configs" / "vban.toml",
|
Path.home() / 'Documents' / 'Voicemeeter' / 'configs' / 'vban.toml',
|
||||||
]
|
):
|
||||||
for filepath in filepaths:
|
if pn.exists():
|
||||||
if filepath.exists():
|
return pn
|
||||||
return filepath
|
|
||||||
|
|
||||||
if filepath := get_filepath():
|
if not (filepath := get_filepath()):
|
||||||
with open(filepath, "rb") as f:
|
raise VBANCMDError('no ip provided and no vban.toml located.')
|
||||||
conn = tomllib.load(f)
|
try:
|
||||||
assert (
|
with open(filepath, 'rb') as f:
|
||||||
"connection" in conn and "ip" in conn["connection"]
|
return tomllib.load(f)['connection']
|
||||||
), "expected [connection][ip] in vban config"
|
except tomllib.TomlDecodeError as e:
|
||||||
return conn["connection"]
|
raise VBANCMDError(f'Error decoding {filepath}: {e}') from e
|
||||||
raise VBANCMDError("no ip provided and no vban.toml located.")
|
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.login()
|
self.login()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
||||||
|
self.logout()
|
||||||
|
|
||||||
def login(self) -> None:
|
def login(self) -> None:
|
||||||
"""Starts the subscriber and updater threads (unless in outbound mode)"""
|
"""Starts the subscriber and updater threads (unless disable_rt_listeners is True) and logs into Voicemeeter."""
|
||||||
if not self.outbound:
|
if not self.disable_rt_listeners:
|
||||||
self.event.info()
|
self.event.info()
|
||||||
|
|
||||||
self.stop_event = threading.Event()
|
self.stop_event = threading.Event()
|
||||||
@@ -108,42 +107,65 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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):
|
def stopped(self):
|
||||||
return self.stop_event.is_set()
|
return self.stop_event is None or self.stop_event.is_set()
|
||||||
|
|
||||||
|
def _send_request(self, payload: str) -> None:
|
||||||
|
"""Sends a request packet over the network and bumps the framecounter."""
|
||||||
|
self.sock.sendto(
|
||||||
|
VbanRequestHeader.encode_with_payload(
|
||||||
|
name=self.streamname,
|
||||||
|
bps_index=self.BPS_OPTS.index(self.bps),
|
||||||
|
channel=self.channel,
|
||||||
|
framecounter=self._framecounter,
|
||||||
|
payload=payload,
|
||||||
|
),
|
||||||
|
(socket.gethostbyname(self.ip), self.port),
|
||||||
|
)
|
||||||
|
self._framecounter = bump_framecounter(self._framecounter)
|
||||||
|
|
||||||
def _set_rt(self, cmd: str, val: Union[str, float]):
|
def _set_rt(self, cmd: str, val: Union[str, float]):
|
||||||
"""Sends a string request command over a network."""
|
"""Sends a string request command over a network."""
|
||||||
self.socks[Socket.request].sendto(
|
self._send_request(f'{cmd}={val};')
|
||||||
self.packet_request.header + f"{cmd}={val};".encode(),
|
|
||||||
(socket.gethostbyname(self.ip), self.port),
|
|
||||||
)
|
|
||||||
self.packet_request.framecounter = (
|
|
||||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
|
||||||
).to_bytes(4, "little")
|
|
||||||
self.cache[cmd] = val
|
self.cache[cmd] = val
|
||||||
|
|
||||||
@script
|
def sendtext(self, script) -> str | None:
|
||||||
def sendtext(self, script):
|
|
||||||
"""Sends a multiple parameter string over a network."""
|
"""Sends a multiple parameter string over a network."""
|
||||||
self.socks[Socket.request].sendto(
|
self._send_request(script)
|
||||||
self.packet_request.header + script.encode(),
|
self.logger.debug(f'sendtext: {script}')
|
||||||
(socket.gethostbyname(self.ip), self.port),
|
|
||||||
|
if self.disable_rt_listeners and script.endswith(('?', '?;')):
|
||||||
|
try:
|
||||||
|
response = VbanMatrixResponseHeader.extract_payload(
|
||||||
|
self.sock.recv(1024)
|
||||||
)
|
)
|
||||||
self.packet_request.framecounter = (
|
return response
|
||||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
except ValueError as e:
|
||||||
).to_bytes(4, "little")
|
self.logger.warning(f'Error extracting matrix response: {e}')
|
||||||
self.logger.debug(f"sendtext: {script}")
|
|
||||||
time.sleep(self.DELAY)
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def type(self) -> str:
|
def type(self) -> str:
|
||||||
"""Returns the type of Voicemeeter installation."""
|
"""Returns the type of Voicemeeter installation."""
|
||||||
return self.public_packet.voicemeetertype
|
return self.public_packets[NBS.zero].voicemeetertype
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def version(self) -> str:
|
def version(self) -> str:
|
||||||
"""Returns Voicemeeter's version as a string"""
|
"""Returns Voicemeeter's version as a string"""
|
||||||
return "{0}.{1}.{2}.{3}".format(*self.public_packet.voicemeeterversion)
|
return '{0}.{1}.{2}.{3}'.format(
|
||||||
|
*self.public_packets[NBS.zero].voicemeeterversion
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pdirty(self):
|
def pdirty(self):
|
||||||
@@ -156,24 +178,13 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
return self._ldirty
|
return self._ldirty
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def public_packet(self):
|
def public_packets(self):
|
||||||
return self._public_packet
|
return self._public_packets
|
||||||
|
|
||||||
def clear_dirty(self) -> None:
|
def clear_dirty(self) -> None:
|
||||||
while self.pdirty:
|
while self.pdirty:
|
||||||
time.sleep(self.DELAY)
|
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):
|
def apply(self, data: dict):
|
||||||
"""
|
"""
|
||||||
Sets all parameters of a dict
|
Sets all parameters of a dict
|
||||||
@@ -181,49 +192,47 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
minor delay between each recursion
|
minor delay between each recursion
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def param(key):
|
def target(key):
|
||||||
obj, m2, *rem = key.split("-")
|
match key.split('-'):
|
||||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
case ['strip' | 'bus' as kls, index] if index.isnumeric():
|
||||||
if obj in ("strip", "bus", "button"):
|
target = getattr(self, kls)
|
||||||
return getattr(self, obj)[index]
|
case [
|
||||||
elif obj == "vban":
|
'vban',
|
||||||
return getattr(getattr(self, obj), f"{m2}stream")[index]
|
'in' | 'instream' | 'out' | 'outstream' as direction,
|
||||||
raise ValueError(obj)
|
index,
|
||||||
|
] if index.isnumeric():
|
||||||
|
target = getattr(
|
||||||
|
self.vban, f'{direction.removesuffix("stream")}stream'
|
||||||
|
)
|
||||||
|
case _:
|
||||||
|
ERR_MSG = f"invalid config key '{key}'"
|
||||||
|
self.logger.error(ERR_MSG)
|
||||||
|
raise ValueError(ERR_MSG)
|
||||||
|
return target[int(index)]
|
||||||
|
|
||||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
[target(key).apply(di).then_wait() for key, di in data.items()]
|
||||||
|
|
||||||
def apply_config(self, name):
|
def apply_config(self, name):
|
||||||
"""applies a config from memory"""
|
"""applies a config from memory"""
|
||||||
ERR_MSG = (
|
ERR_MSG = (
|
||||||
f"No config with name '{name}' is loaded into memory",
|
f"No config with name '{name}' is loaded into memory",
|
||||||
f"Known configs: {list(self.configs.keys())}",
|
f'Known configs: {list(self.configs.keys())}',
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
config = self.configs[name]
|
config = self.configs[name]
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self.logger.error(("\n").join(ERR_MSG))
|
self.logger.error(('\n').join(ERR_MSG))
|
||||||
raise VBANCMDError(("\n").join(ERR_MSG)) from e
|
raise VBANCMDError(('\n').join(ERR_MSG)) from e
|
||||||
|
|
||||||
if "extends" in config:
|
if 'extends' in config:
|
||||||
extended = config["extends"]
|
extended = config['extends']
|
||||||
config = {
|
config = {
|
||||||
k: v
|
k: v
|
||||||
for k, v in deep_merge(self.configs[extended], config)
|
for k, v in deep_merge(self.configs[extended], config)
|
||||||
if k not in ("extends")
|
if k not in ('extends')
|
||||||
}
|
}
|
||||||
self.logger.debug(
|
self.logger.debug(
|
||||||
f"profile '{name}' extends '{extended}', profiles merged.."
|
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||||
)
|
)
|
||||||
self.apply(config)
|
self.apply(config)
|
||||||
self.logger.info(f"Profile '{name}' applied!")
|
self.logger.info(f"Profile '{name}' applied!")
|
||||||
|
|
||||||
def logout(self) -> None:
|
|
||||||
if not self.stopped():
|
|
||||||
self.logger.debug("events thread shutdown started")
|
|
||||||
self.stop_event.set()
|
|
||||||
self.subscriber.join() # wait for subscriber thread to complete cycle
|
|
||||||
[sock.close() for sock in self.socks]
|
|
||||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
|
||||||
self.logout()
|
|
||||||
|
|||||||
@@ -2,11 +2,18 @@ import logging
|
|||||||
import socket
|
import socket
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
from .enums import NBS
|
||||||
from .error import VBANCMDConnectionError
|
from .error import VBANCMDConnectionError
|
||||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
from .packet.headers import (
|
||||||
from .util import Socket
|
HEADER_SIZE,
|
||||||
|
VbanPacket,
|
||||||
|
VbanResponseHeader,
|
||||||
|
VbanSubscribeHeader,
|
||||||
|
)
|
||||||
|
from .packet.nbs0 import VbanPacketNBS0
|
||||||
|
from .packet.nbs1 import VbanPacketNBS1
|
||||||
|
from .util import bump_framecounter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -15,29 +22,29 @@ class Subscriber(threading.Thread):
|
|||||||
"""fire a subscription packet every 10 seconds"""
|
"""fire a subscription packet every 10 seconds"""
|
||||||
|
|
||||||
def __init__(self, remote, stop_event):
|
def __init__(self, remote, stop_event):
|
||||||
super().__init__(name="subscriber", daemon=False)
|
super().__init__(name='subscriber', daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.packet = SubscribeHeader()
|
self._framecounter = 0
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while not self.stopped():
|
while not self.stopped():
|
||||||
try:
|
try:
|
||||||
self._remote.socks[Socket.register].sendto(
|
for nbs in NBS:
|
||||||
self.packet.header,
|
sub_packet = VbanSubscribeHeader().to_bytes(nbs, self._framecounter)
|
||||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
self._remote.sock.sendto(
|
||||||
|
sub_packet, (self._remote.ip, self._remote.port)
|
||||||
)
|
)
|
||||||
self.packet.framecounter = (
|
self._framecounter = bump_framecounter(self._framecounter)
|
||||||
int.from_bytes(self.packet.framecounter, "little") + 1
|
|
||||||
).to_bytes(4, "little")
|
|
||||||
self.wait_until_stopped(10)
|
self.wait_until_stopped(10)
|
||||||
except socket.gaierror as e:
|
except socket.gaierror as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f'{type(e).__name__}: {e}')
|
||||||
raise VBANCMDConnectionError(
|
raise VBANCMDConnectionError(
|
||||||
f"unable to resolve hostname {self._remote.ip}"
|
f'unable to resolve hostname {self._remote.ip}'
|
||||||
) from e
|
) from e
|
||||||
self.logger.debug(f"terminating {self.name} thread")
|
self.logger.debug(f'terminating {self.name} thread')
|
||||||
|
|
||||||
def stopped(self):
|
def stopped(self):
|
||||||
return self.stop_event.is_set()
|
return self.stop_event.is_set()
|
||||||
@@ -54,93 +61,85 @@ class Producer(threading.Thread):
|
|||||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||||
|
|
||||||
def __init__(self, remote, queue, stop_event):
|
def __init__(self, remote, queue, stop_event):
|
||||||
super().__init__(name="producer", daemon=False)
|
super().__init__(name='producer', daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.stop_event = stop_event
|
self.stop_event = stop_event
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.packet_expected = VbanRtPacketHeader()
|
self._remote.sock.settimeout(self._remote.timeout)
|
||||||
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
self._remote._public_packets = [None] * (max(NBS) + 1)
|
||||||
self._remote.socks[Socket.response].bind(
|
_pp = self._get_rt()
|
||||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
self._remote._public_packets[_pp.nbs] = _pp
|
||||||
)
|
|
||||||
self._remote._public_packet = self._get_rt()
|
|
||||||
(
|
(
|
||||||
self._remote.cache["strip_level"],
|
self._remote.cache['strip_level'],
|
||||||
self._remote.cache["bus_level"],
|
self._remote.cache['bus_level'],
|
||||||
) = self._remote._get_levels(self._remote.public_packet)
|
) = self._remote.public_packets[NBS.zero].levels
|
||||||
|
|
||||||
def _get_rt(self) -> VbanRtPacket:
|
def _get_rt(self) -> VbanPacket:
|
||||||
"""Attempt to fetch data packet until a valid one found"""
|
"""Attempt to fetch data packet until a valid one found"""
|
||||||
|
|
||||||
def fget():
|
while True:
|
||||||
data = None
|
if resp := self._fetch_rt_packet():
|
||||||
while not data:
|
return resp
|
||||||
data = self._fetch_rt_packet()
|
|
||||||
return data
|
|
||||||
|
|
||||||
return fget()
|
def _fetch_rt_packet(self) -> VbanPacket | None:
|
||||||
|
|
||||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
|
||||||
try:
|
try:
|
||||||
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
data, _ = self._remote.sock.recvfrom(2048)
|
||||||
# do we have packet data?
|
if len(data) < HEADER_SIZE:
|
||||||
if len(data) > HEADER_SIZE:
|
return
|
||||||
# is the packet of type VBAN RT response?
|
|
||||||
if self.packet_expected.header == data[:HEADER_SIZE]:
|
|
||||||
return VbanRtPacket(
|
|
||||||
_kind=self._remote.kind,
|
|
||||||
_voicemeeterType=data[28:29],
|
|
||||||
_reserved=data[29:30],
|
|
||||||
_buffersize=data[30:32],
|
|
||||||
_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],
|
|
||||||
)
|
|
||||||
except TimeoutError as e:
|
except TimeoutError as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f'{type(e).__name__}: {e}')
|
||||||
raise VBANCMDConnectionError(
|
raise VBANCMDConnectionError(
|
||||||
f"timeout waiting for RtPacket from {self._remote.ip}"
|
f'timeout waiting for response from {self._remote.ip}:{self._remote.port}'
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
try:
|
||||||
|
header = VbanResponseHeader.from_bytes(data[:HEADER_SIZE])
|
||||||
|
except ValueError as e:
|
||||||
|
self.logger.debug(f'Error parsing response packet: {e}')
|
||||||
|
return None
|
||||||
|
|
||||||
|
match header.format_nbs:
|
||||||
|
case NBS.zero:
|
||||||
|
return VbanPacketNBS0.from_bytes(
|
||||||
|
nbs=NBS.zero, kind=self._remote.kind, data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
case NBS.one:
|
||||||
|
return VbanPacketNBS1.from_bytes(
|
||||||
|
nbs=NBS.one, kind=self._remote.kind, data=data
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def stopped(self):
|
def stopped(self):
|
||||||
return self.stop_event.is_set()
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while not self.stopped():
|
while not self.stopped():
|
||||||
|
pdirty = ldirty = False
|
||||||
_pp = self._get_rt()
|
_pp = self._get_rt()
|
||||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
match _pp.nbs:
|
||||||
|
case NBS.zero:
|
||||||
ldirty = _pp.ldirty(
|
ldirty = _pp.ldirty(
|
||||||
self._remote.cache["strip_level"], self._remote.cache["bus_level"]
|
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:
|
if pdirty or ldirty:
|
||||||
self._remote._public_packet = _pp
|
self._remote._public_packets[_pp.nbs] = _pp
|
||||||
self._remote._pdirty = pdirty
|
self._remote._pdirty = pdirty
|
||||||
self._remote._ldirty = ldirty
|
self._remote._ldirty = ldirty
|
||||||
|
|
||||||
if self._remote.event.pdirty:
|
if self._remote.event.pdirty:
|
||||||
self.queue.put("pdirty")
|
self.queue.put('pdirty')
|
||||||
if self._remote.event.ldirty:
|
if self._remote.event.ldirty:
|
||||||
self.queue.put("ldirty")
|
self.queue.put('ldirty')
|
||||||
time.sleep(self._remote.ratelimit)
|
# time.sleep(self._remote.ratelimit)
|
||||||
self.logger.debug(f"terminating {self.name} thread")
|
self.logger.debug(f'terminating {self.name} thread')
|
||||||
self.queue.put(None)
|
self.queue.put(None)
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +151,7 @@ class Updater(threading.Thread):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, remote, queue):
|
def __init__(self, remote, queue):
|
||||||
super().__init__(name="updater", daemon=True)
|
super().__init__(name='updater', daemon=True)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
@@ -166,19 +165,16 @@ class Updater(threading.Thread):
|
|||||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||||
"""
|
"""
|
||||||
while event := self.queue.get():
|
while event := self.queue.get():
|
||||||
if event == "pdirty" and self._remote.pdirty:
|
if event == 'pdirty' and self._remote.pdirty:
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
elif event == "ldirty" and self._remote.ldirty:
|
elif event == 'ldirty' and self._remote.ldirty:
|
||||||
self._remote._strip_comp, self._remote._bus_comp = (
|
self._remote._strip_comp, self._remote._bus_comp = (
|
||||||
self._remote._public_packet._strip_comp,
|
self._remote._public_packets[NBS.zero]._strip_comp,
|
||||||
self._remote._public_packet._bus_comp,
|
self._remote._public_packets[NBS.zero]._bus_comp,
|
||||||
)
|
)
|
||||||
(
|
(
|
||||||
self._remote.cache["strip_level"],
|
self._remote.cache['strip_level'],
|
||||||
self._remote.cache["bus_level"],
|
self._remote.cache['bus_level'],
|
||||||
) = (
|
) = self._remote.public_packets[NBS.zero].levels
|
||||||
self._remote._public_packet.inputlevels,
|
|
||||||
self._remote._public_packet.outputlevels,
|
|
||||||
)
|
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
self.logger.debug(f"terminating {self.name} thread")
|
self.logger.debug(f'terminating {self.name} thread')
|
||||||
|
|||||||
Reference in New Issue
Block a user