mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2025-05-21 17:50:22 +01:00
Compare commits
28 Commits
f6218d2032
...
8b912a2d08
Author | SHA1 | Date | |
---|---|---|---|
8b912a2d08 | |||
d2a5fe197e | |||
0970bfe0b5 | |||
54041503c9 | |||
9d015755eb | |||
ca9a31c94a | |||
7a3abfc372 | |||
37a9c88867 | |||
df7996a846 | |||
3f5dc7c376 | |||
05cbc432b2 | |||
174d95d08d | |||
fc324fecc4 | |||
449cb9b3c1 | |||
cdccc603d1 | |||
a8bb9711af | |||
5bb0c2731e | |||
372dba0b6b | |||
226fc5ead7 | |||
9196a4e267 | |||
8485992495 | |||
91e49cbb55 | |||
3c85903554 | |||
a730edc2c2 | |||
90acafe95b | |||
5f4fdcb0eb | |||
d5219d66f7 | |||
c74d827154 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -157,3 +157,5 @@ quick.py
|
||||
#config
|
||||
config.toml
|
||||
vban.toml
|
||||
|
||||
.vscode/
|
30
CHANGELOG.md
30
CHANGELOG.md
@ -11,6 +11,36 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.0.0] - 2023-06-25
|
||||
|
||||
This update introduces some breaking changes:
|
||||
|
||||
### Changed
|
||||
|
||||
- `strip[i].comp` now references StripComp class
|
||||
- To change the comp knob you should now use the property `strip[i].comp.knob`
|
||||
- `strip[i].gate` now references StripGate class
|
||||
|
||||
- To change the gate knob you should now use the property `strip[i].gate.knob`
|
||||
|
||||
- `bus[i].eq` now references BusEQ class
|
||||
|
||||
- To set bus[i].{eq,eq_ab} as before you should now use bus[i].eq.on and bus[i].eq.ab
|
||||
|
||||
- new error class `VBANCMDConnectionError` raised when a connection fails or times out.
|
||||
|
||||
There are other non-breaking changes:
|
||||
|
||||
### Changed
|
||||
|
||||
- now using a producer thread to send events to the updater thread.
|
||||
- factory.request_vbancmd_obj simply raises a `VBANCMDError` if passed an incorrect kind.
|
||||
- module level loggers implemented (with class loggers as child loggers)
|
||||
|
||||
### Added
|
||||
|
||||
- `strip[i].eq` added to PhysicalStrip
|
||||
|
||||
## [1.8.0]
|
||||
|
||||
### Added
|
||||
|
106
README.md
106
README.md
@ -18,9 +18,9 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## Tested against
|
||||
|
||||
- Basic 1.0.8.4
|
||||
- Banana 2.0.6.4
|
||||
- Potato 3.0.2.4
|
||||
- Basic 1.0.8.8
|
||||
- Banana 2.0.6.8
|
||||
- Potato 3.0.2.8
|
||||
|
||||
## Requirements
|
||||
|
||||
@ -71,19 +71,19 @@ class ManyThings:
|
||||
|
||||
def other_things(self):
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
self.vban.bus[4].eq.on = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq.on}",
|
||||
)
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
def main():
|
||||
kind_id = "banana"
|
||||
KIND_ID = "banana"
|
||||
|
||||
with vban_cmd.api(
|
||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
KIND_ID, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
) as vban:
|
||||
do = ManyThings(vban)
|
||||
do.things()
|
||||
@ -93,7 +93,7 @@ def main():
|
||||
vban.apply(
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"bus-2": {"mute": True, "eq": {"on": True}},
|
||||
}
|
||||
)
|
||||
|
||||
@ -104,9 +104,9 @@ if __name__ == "__main__":
|
||||
|
||||
Otherwise you must remember to call `vban.login()`, `vban.logout()` at the start/end of your code.
|
||||
|
||||
## `kind_id`
|
||||
## `KIND_ID`
|
||||
|
||||
Pass the kind of Voicemeeter as an argument. kind_id may be:
|
||||
Pass the kind of Voicemeeter as an argument. KIND_ID may be:
|
||||
|
||||
- `basic`
|
||||
- `banana`
|
||||
@ -124,8 +124,6 @@ The following properties are available.
|
||||
- `label`: string
|
||||
- `gain`: float, -60 to 12
|
||||
- `A1 - A5`, `B1 - B3`: boolean
|
||||
- `comp`: float, from 0.0 to 10.0
|
||||
- `gate`: float, from 0.0 to 10.0
|
||||
- `limit`: int, from -40 to 12
|
||||
|
||||
example:
|
||||
@ -152,6 +150,69 @@ vban.strip[5].appmute("Spotify", True)
|
||||
vban.strip[5].appgain("Spotify", 0.5)
|
||||
```
|
||||
|
||||
##### Strip.Comp
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
- `gainin`: float, from -24.0 to 24.0
|
||||
- `ratio`: float, from 1.0 to 8.0
|
||||
- `threshold`: float, from -40.0 to -3.0
|
||||
- `attack`: float, from 0.0 to 200.0
|
||||
- `release`: float, from 0.0 to 5000.0
|
||||
- `knee`: float, from 0.0 to 1.0
|
||||
- `gainout`: float, from -24.0 to 24.0
|
||||
- `makeup`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
print(vm.strip[4].comp.knob)
|
||||
```
|
||||
|
||||
Strip Comp properties are defined as write only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Gate
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
- `threshold`: float, from -60.0 to -10.0
|
||||
- `damping`: float, from -60.0 to -10.0
|
||||
- `bpsidechain`: int, from 100 to 4000
|
||||
- `attack`: float, from 0.0 to 1000.0
|
||||
- `hold`: float, from 0.0 to 5000.0
|
||||
- `release`: float, from 0.0 to 5000.0
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vm.strip[2].gate.attack = 300.8
|
||||
```
|
||||
|
||||
Strip Gate properties are defined as write only, potato version only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Denoiser
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `knob`: float, from 0.0 to 10.0
|
||||
|
||||
strip.denoiser properties are defined as write only, potato version only.
|
||||
|
||||
##### Strip.EQ
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `ab`: boolean
|
||||
|
||||
Strip EQ properties are defined as write only, potato version only.
|
||||
|
||||
##### Gainlayers
|
||||
|
||||
- `gain`: float, from -60.0 to 12.0
|
||||
@ -183,8 +244,6 @@ Level properties will return -200.0 if no audio detected.
|
||||
The following properties are available.
|
||||
|
||||
- `mono`: boolean
|
||||
- `eq`: boolean
|
||||
- `eq_ab`: boolean
|
||||
- `mute`: boolean
|
||||
- `label`: string
|
||||
- `gain`: float, -60 to 12
|
||||
@ -196,6 +255,13 @@ vban.bus[4].eq = true
|
||||
print(vban.bus[0].label)
|
||||
```
|
||||
|
||||
##### Bus.EQ
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `ab`: boolean
|
||||
|
||||
##### Modes
|
||||
|
||||
The following properties are available.
|
||||
@ -325,9 +391,8 @@ opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
"subs": {"ldirty": True},
|
||||
}
|
||||
with vban_cmd.api('banana', **opts) as vban:
|
||||
with vban_cmd.api('banana', ldirty=True, **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
@ -386,16 +451,15 @@ print(vban.event.get())
|
||||
|
||||
## VbanCmd class
|
||||
|
||||
`vban_cmd.api(kind_id: str, **opts: dict)`
|
||||
`vban_cmd.api(kind_id: str, **opts)`
|
||||
|
||||
You may pass the following optional keyword arguments:
|
||||
|
||||
- `ip`: str, ip or hostname of remote machine
|
||||
- `streamname`: str, name of the stream to connect to.
|
||||
- `port`: int=6980, vban udp port of remote machine.
|
||||
- `subs`: dict={"pdirty": True, "ldirty": False}, controls which updates to listen for.
|
||||
- `pdirty`: parameter updates
|
||||
- `ldirty`: level updates
|
||||
- `pdirty`: parameter updates
|
||||
- `ldirty`: level updates
|
||||
|
||||
#### `vban.pdirty`
|
||||
|
||||
@ -415,7 +479,7 @@ vban.sendtext("Strip[0].Mute=1;Bus[0].Mono=1")
|
||||
|
||||
#### `vban.public_packet`
|
||||
|
||||
Returns a Voicemeeter rt data packet object. Designed to be used internally by the interface but available for parsing through this read only property object. States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||
Returns a `VbanRtPacket`. Designed to be used internally by the interface but available for parsing through this read only property object. States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||
|
||||
### `Errors`
|
||||
|
||||
|
@ -2,12 +2,12 @@
|
||||
label = "PhysStrip0"
|
||||
A1 = true
|
||||
gain = -8.8
|
||||
comp = 3.2
|
||||
comp.knob = 3.2
|
||||
|
||||
[strip-1]
|
||||
label = "PhysStrip1"
|
||||
B1 = true
|
||||
gate = 4.1
|
||||
gate.knob = 4.1
|
||||
|
||||
[strip-2]
|
||||
label = "PhysStrip2"
|
||||
@ -31,12 +31,12 @@ mono = true
|
||||
|
||||
[bus-2]
|
||||
label = "PhysBus2"
|
||||
eq = true
|
||||
eq.on = true
|
||||
mode = "composite"
|
||||
|
||||
[bus-3]
|
||||
label = "VirtBus0"
|
||||
eq_ab = true
|
||||
eq.ab = true
|
||||
mode = "upmix61"
|
||||
|
||||
[bus-4]
|
||||
|
@ -2,12 +2,12 @@
|
||||
label = "PhysStrip0"
|
||||
A1 = true
|
||||
gain = -8.8
|
||||
comp = 3.2
|
||||
comp.knob = 3.2
|
||||
|
||||
[strip-1]
|
||||
label = "PhysStrip1"
|
||||
B1 = true
|
||||
gate = 4.1
|
||||
gate.knob = 4.1
|
||||
|
||||
[strip-2]
|
||||
label = "PhysStrip2"
|
||||
@ -47,7 +47,7 @@ mono = true
|
||||
|
||||
[bus-2]
|
||||
label = "PhysBus2"
|
||||
eq = true
|
||||
eq.on = true
|
||||
|
||||
[bus-3]
|
||||
label = "PhysBus3"
|
||||
@ -59,7 +59,7 @@ mode = "composite"
|
||||
|
||||
[bus-5]
|
||||
label = "VirtBus0"
|
||||
eq_ab = true
|
||||
eq.ab = true
|
||||
|
||||
[bus-6]
|
||||
label = "VirtBus1"
|
||||
|
13
examples/gui/README.md
Normal file
13
examples/gui/README.md
Normal file
@ -0,0 +1,13 @@
|
||||
## About
|
||||
|
||||
A single channel GUI demonstrating controls for the first virtual strip if Voicemeeter Banana.
|
||||
|
||||
This example demonstrates (to an extent) two way communication.
|
||||
- Sending parameters values to the Voicemeeter driver.
|
||||
- Receiving level updates
|
||||
|
||||
Parameter updates (pdirty) events are not being received so changing a UI element on the main Voicemeeter app will not be reflected in the example GUI.
|
||||
|
||||
## Use
|
||||
|
||||
Simply run the script and try the controls.
|
100
examples/gui/__main__.py
Normal file
100
examples/gui/__main__.py
Normal file
@ -0,0 +1,100 @@
|
||||
import logging
|
||||
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
|
||||
class App(tk.Tk):
|
||||
def __init__(self, vban):
|
||||
super().__init__()
|
||||
self.vban = vban
|
||||
self.title(f"{vban} - version {vban.version}")
|
||||
self.vban.observer.add(self.on_ldirty)
|
||||
|
||||
# create widget variables
|
||||
self.button_var = tk.BooleanVar(value=vban.strip[3].mute)
|
||||
self.slider_var = tk.DoubleVar(value=vban.strip[3].gain)
|
||||
self.meter_var = tk.DoubleVar(value=self._get_level())
|
||||
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
|
||||
|
||||
# initialize style table
|
||||
self.style = ttk.Style()
|
||||
self.style.theme_use("clam")
|
||||
self.style.configure(
|
||||
"Mute.TButton", foreground="#cd5c5c" if vban.strip[3].mute else "#5a5a5a"
|
||||
)
|
||||
|
||||
# create labelframe and grid it onto the mainframe
|
||||
self.labelframe = tk.LabelFrame(text=self.vban.strip[3].label)
|
||||
self.labelframe.grid(padx=1)
|
||||
|
||||
# create slider and grid it onto the labelframe
|
||||
slider = ttk.Scale(
|
||||
self.labelframe,
|
||||
from_=12,
|
||||
to_=-60,
|
||||
orient="vertical",
|
||||
variable=self.slider_var,
|
||||
command=lambda arg: self.on_slider_move(arg),
|
||||
)
|
||||
slider.grid(
|
||||
column=0,
|
||||
row=0,
|
||||
)
|
||||
|
||||
# create level meter and grid it onto the labelframe
|
||||
level_meter = ttk.Progressbar(
|
||||
self.labelframe,
|
||||
orient="vertical",
|
||||
variable=self.meter_var,
|
||||
maximum=72,
|
||||
mode="determinate",
|
||||
)
|
||||
level_meter.grid(column=1, row=0)
|
||||
|
||||
# create gainlabel and grid it onto the labelframe
|
||||
gainlabel = ttk.Label(self.labelframe, textvariable=self.gainlabel_var)
|
||||
gainlabel.grid(column=0, row=1, columnspan=2)
|
||||
|
||||
# create button and grid it onto the labelframe
|
||||
button = ttk.Button(
|
||||
self.labelframe,
|
||||
text="Mute",
|
||||
style="Mute.TButton",
|
||||
command=lambda: self.on_button_press(),
|
||||
)
|
||||
button.grid(column=0, row=2, columnspan=2, padx=1, pady=2)
|
||||
|
||||
# define callbacks
|
||||
|
||||
def on_slider_move(self, *args):
|
||||
val = round(self.slider_var.get(), 1)
|
||||
self.vban.strip[3].gain = val
|
||||
self.gainlabel_var.set(val)
|
||||
|
||||
def on_button_press(self):
|
||||
self.button_var.set(not self.button_var.get())
|
||||
self.vban.strip[3].mute = self.button_var.get()
|
||||
self.style.configure(
|
||||
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
||||
)
|
||||
|
||||
def _get_level(self):
|
||||
val = max(self.vban.strip[3].levels.prefader)
|
||||
return 0 if self.button_var.get() else 72 + val - 12 + self.slider_var.get()
|
||||
|
||||
def on_ldirty(self):
|
||||
self.meter_var.set(self._get_level())
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api("banana", ldirty=True) as vban:
|
||||
app = App(vban)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -40,10 +40,12 @@ Make sure you have established a working connection to OBS and the remote Voicem
|
||||
|
||||
Run the script, change OBS scenes and watch Voicemeeter parameters change.
|
||||
|
||||
Pressing `<Enter>` will exit.
|
||||
Closing OBS will end the script.
|
||||
|
||||
## Notes
|
||||
|
||||
All but `vban_cmd.iremote` logs are filtered out. Log in DEBUG mode.
|
||||
|
||||
This script can be run from a Linux host since the vban-cmd interface relies on UDP packets and obsws-python runs over websockets.
|
||||
|
||||
You could for example, set this up to run in the background on a home server such as a Raspberry Pi.
|
||||
|
@ -1,16 +1,41 @@
|
||||
import logging
|
||||
import time
|
||||
from logging import config
|
||||
|
||||
import obsws_python as obsws
|
||||
|
||||
import obsws_python as obs
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
config.dictConfig(
|
||||
{
|
||||
"version": 1,
|
||||
"formatters": {
|
||||
"standard": {
|
||||
"format": "%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s"
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"stream": {
|
||||
"level": "DEBUG",
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "standard",
|
||||
}
|
||||
},
|
||||
"loggers": {"vban_cmd.iremote": {"handlers": ["stream"], "level": "DEBUG"}},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
self.client = obs.EventClient()
|
||||
self.client.callback.register(self.on_current_program_scene_changed)
|
||||
self.client = obsws.EventClient()
|
||||
self.client.callback.register(
|
||||
(
|
||||
self.on_current_program_scene_changed,
|
||||
self.on_exit_started,
|
||||
)
|
||||
)
|
||||
self.is_running = True
|
||||
|
||||
def on_start(self):
|
||||
self.vban.strip[0].mute = True
|
||||
@ -50,13 +75,16 @@ class Observer:
|
||||
if fn := fget(scene):
|
||||
fn()
|
||||
|
||||
def on_exit_started(self, _):
|
||||
self.client.unsubscribe()
|
||||
self.is_running = False
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api("potato", sync=True) as vban:
|
||||
obs = Observer(vban)
|
||||
while cmd := input("<Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
with vban_cmd.api("potato") as vban:
|
||||
observer = Observer(vban)
|
||||
while observer.is_running:
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
7
examples/obs/setup.py
Normal file
7
examples/obs/setup.py
Normal file
@ -0,0 +1,7 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name="obs",
|
||||
description="OBS Example",
|
||||
install_requires=["obsws-python"],
|
||||
)
|
@ -5,33 +5,30 @@ import vban_cmd
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Observer:
|
||||
class App:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
# register your app as event observer
|
||||
self.vban.subject.add(self)
|
||||
# enable level updates, since they are disabled by default.
|
||||
self.vban.event.ldirty = True
|
||||
self.vban.observer.add(self)
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
def on_update(self, event):
|
||||
if event == "pdirty":
|
||||
print("pdirty!")
|
||||
elif subject == "ldirty":
|
||||
elif event == "ldirty":
|
||||
for bus in self.vban.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
|
||||
|
||||
def main():
|
||||
kind_id = "potato"
|
||||
KIND_ID = "banana"
|
||||
|
||||
with vban_cmd.api(kind_id) as vban:
|
||||
Observer(vban)
|
||||
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True) as vban:
|
||||
App(vban)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
125
poetry.lock
generated
125
poetry.lock
generated
@ -33,6 +33,22 @@ d = ["aiohttp (>=3.7.4)"]
|
||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||
uvloop = ["uvloop (>=0.15.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "cachetools"
|
||||
version = "5.3.1"
|
||||
description = "Extensible memoizing collections and decorators"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "chardet"
|
||||
version = "5.1.0"
|
||||
description = "Universal encoding detector for Python 3"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "click"
|
||||
version = "8.1.3"
|
||||
@ -46,11 +62,31 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.5"
|
||||
version = "0.4.6"
|
||||
description = "Cross-platform colored terminal text."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.3.6"
|
||||
description = "Distribution utilities"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.12.2"
|
||||
description = "A platform independent file lock."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
@ -84,14 +120,11 @@ python-versions = "*"
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "21.3"
|
||||
version = "23.1"
|
||||
description = "Core utilities for Python packages"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.dependencies]
|
||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
@ -103,15 +136,15 @@ python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "2.5.2"
|
||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
version = "3.7.0"
|
||||
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
@ -122,8 +155,8 @@ optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
testing = ["pytest-benchmark", "pytest"]
|
||||
dev = ["tox", "pre-commit"]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
@ -134,15 +167,20 @@ optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
|
||||
[[package]]
|
||||
name = "pyparsing"
|
||||
version = "3.0.9"
|
||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||
name = "pyproject-api"
|
||||
version = "1.5.2"
|
||||
description = "API to interact with the python pyproject.toml based projects"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.6.8"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
packaging = ">=23.1"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
diagrams = ["railroad-diagrams", "jinja2"]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
@ -194,16 +232,61 @@ category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "tox"
|
||||
version = "4.6.3"
|
||||
description = "tox is a generic virtualenv management and test command line tool"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
cachetools = ">=5.3.1"
|
||||
chardet = ">=5.1"
|
||||
colorama = ">=0.4.6"
|
||||
filelock = ">=3.12.2"
|
||||
packaging = ">=23.1"
|
||||
platformdirs = ">=3.5.3"
|
||||
pluggy = ">=1"
|
||||
pyproject-api = ">=1.5.2"
|
||||
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||
virtualenv = ">=20.23.1"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
||||
|
||||
[[package]]
|
||||
name = "virtualenv"
|
||||
version = "20.23.1"
|
||||
description = "Virtual Python Environment builder"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
distlib = ">=0.3.6,<1"
|
||||
filelock = ">=3.12,<4"
|
||||
platformdirs = ">=3.5.1,<4"
|
||||
|
||||
[package.extras]
|
||||
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||
test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
|
||||
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
||||
|
||||
[metadata.files]
|
||||
attrs = []
|
||||
black = []
|
||||
cachetools = []
|
||||
chardet = []
|
||||
click = []
|
||||
colorama = []
|
||||
distlib = []
|
||||
filelock = []
|
||||
iniconfig = []
|
||||
isort = []
|
||||
mypy-extensions = []
|
||||
@ -212,8 +295,10 @@ pathspec = []
|
||||
platformdirs = []
|
||||
pluggy = []
|
||||
py = []
|
||||
pyparsing = []
|
||||
pyproject-api = []
|
||||
pytest = []
|
||||
pytest-randomly = []
|
||||
pytest-repeat = []
|
||||
tomli = []
|
||||
tox = []
|
||||
virtualenv = []
|
||||
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vban-cmd"
|
||||
version = "1.8.1"
|
||||
version = "2.0.0"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
@ -18,11 +18,26 @@ pytest-randomly = "^3.12.0"
|
||||
pytest-repeat = "^0.9.1"
|
||||
black = "^22.3.0"
|
||||
isort = "^5.10.1"
|
||||
tox = "^4.6.3"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
gui = "scripts:ex_gui"
|
||||
obs = "scripts:ex_obs"
|
||||
observer = "scripts:ex_observer"
|
||||
test = "scripts:test"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310,py311
|
||||
|
||||
[testenv]
|
||||
allowlist_externals = poetry
|
||||
commands =
|
||||
poetry install -v
|
||||
poetry run pytest tests/
|
||||
"""
|
||||
|
@ -2,6 +2,11 @@ import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_gui():
|
||||
path = Path.cwd() / "examples" / "gui" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
path = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
@ -10,3 +15,7 @@ def ex_obs():
|
||||
def ex_observer():
|
||||
path = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def test():
|
||||
subprocess.run(["tox"])
|
||||
|
@ -3,23 +3,21 @@ import sys
|
||||
from dataclasses import dataclass
|
||||
|
||||
import vban_cmd
|
||||
from vban_cmd.kinds import KindId, kinds_all
|
||||
from vban_cmd.kinds import KindId
|
||||
from vban_cmd.kinds import request_kind_map as kindmap
|
||||
|
||||
# let's keep things random
|
||||
kind_id = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
|
||||
opts = {
|
||||
"ip": "ws.local",
|
||||
"streamname": "workstation",
|
||||
"ip": "testing.local",
|
||||
"streamname": "testing",
|
||||
"port": 6990,
|
||||
"bps": 0,
|
||||
"sync": True,
|
||||
}
|
||||
|
||||
vbans = {kind.name: vban_cmd.api(kind.name, **opts) for kind in kinds_all}
|
||||
tests = vbans[kind_id]
|
||||
kind = kindmap(kind_id)
|
||||
vban = vban_cmd.api(KIND_ID, **opts)
|
||||
kind = kindmap(KIND_ID)
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -42,9 +40,9 @@ data = Data()
|
||||
|
||||
def setup_module():
|
||||
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
||||
tests.login()
|
||||
tests.command.reset()
|
||||
vban.login()
|
||||
vban.command.reset()
|
||||
|
||||
|
||||
def teardown_module():
|
||||
tests.logout()
|
||||
vban.logout()
|
||||
|
@ -1,8 +1,6 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestSetAndGetBoolHigher:
|
||||
@ -12,18 +10,18 @@ class TestSetAndGetBoolHigher:
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
tests.apply_config("example")
|
||||
vban.apply_config("example")
|
||||
|
||||
def test_it_tests_config_string(self):
|
||||
assert "PhysStrip" in tests.strip[data.phys_in].label
|
||||
assert "VirtStrip" in tests.strip[data.virt_in].label
|
||||
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 tests.strip[0].A1 == True
|
||||
assert vban.strip[0].A1 == True
|
||||
|
||||
@pytest.mark.skipif(
|
||||
"not config.getoption('--run-slow')",
|
||||
reason="Only run when --run-slow is given",
|
||||
)
|
||||
def test_it_tests_config_busmode(self):
|
||||
assert tests.bus[data.phys_out].mode.get() == "composite"
|
||||
assert vban.bus[data.phys_out].mode.get() == "composite"
|
||||
|
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
class TestRemoteFactories:
|
||||
@ -11,33 +11,33 @@ class TestRemoteFactories:
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_basic(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
|
||||
assert len(tests.strip) == 3
|
||||
assert len(tests.bus) == 2
|
||||
assert len(vban.strip) == 3
|
||||
assert len(vban.bus) == 2
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "banana",
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_banana(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
|
||||
assert len(tests.strip) == 5
|
||||
assert len(tests.bus) == 5
|
||||
assert len(vban.strip) == 5
|
||||
assert len(vban.bus) == 5
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Skip test if kind is not basic",
|
||||
)
|
||||
def test_it_tests_remote_attrs_for_potato(self):
|
||||
assert hasattr(tests, "strip")
|
||||
assert hasattr(tests, "bus")
|
||||
assert hasattr(tests, "command")
|
||||
assert hasattr(vban, "strip")
|
||||
assert hasattr(vban, "bus")
|
||||
assert hasattr(vban, "command")
|
||||
|
||||
assert len(tests.strip) == 8
|
||||
assert len(tests.bus) == 8
|
||||
assert len(vban.strip) == 8
|
||||
assert len(vban.bus) == 8
|
||||
|
@ -1,6 +1,6 @@
|
||||
import pytest
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", [False, True])
|
||||
@ -17,8 +17,8 @@ class TestSetAndGetBoolHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name == "banana",
|
||||
@ -31,23 +31,22 @@ class TestSetAndGetBoolHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params_mc(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,param",
|
||||
[
|
||||
(data.phys_out, "eq"),
|
||||
(data.phys_out, "mute"),
|
||||
(data.virt_out, "eq_ab"),
|
||||
(data.virt_out, "sel"),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
assert hasattr(vban.bus[index], param)
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
||||
""" bus modes tests, physical and virtual """
|
||||
|
||||
@ -66,8 +65,8 @@ class TestSetAndGetBoolHigher:
|
||||
# here it only makes sense to set/get bus modes as True
|
||||
if not value:
|
||||
value = True
|
||||
setattr(tests.bus[index].mode, param, value)
|
||||
assert getattr(tests.bus[index].mode, param) == value
|
||||
setattr(vban.bus[index].mode, param, value)
|
||||
assert getattr(vban.bus[index].mode, param) == value
|
||||
|
||||
""" command tests """
|
||||
|
||||
@ -76,7 +75,7 @@ class TestSetAndGetBoolHigher:
|
||||
[("lock")],
|
||||
)
|
||||
def test_it_sets_command_bool_params(self, param, value):
|
||||
setattr(tests.command, param, value)
|
||||
setattr(vban.command, param, value)
|
||||
|
||||
|
||||
class TestSetAndGetIntHigher:
|
||||
@ -94,8 +93,8 @@ class TestSetAndGetIntHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
|
||||
class TestSetAndGetFloatHigher:
|
||||
@ -113,15 +112,15 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,value",
|
||||
[(data.phys_in, 2), (data.phys_in, 2), (data.virt_in, 8), (data.virt_in, 8)],
|
||||
)
|
||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||
assert len(tests.strip[index].levels.prefader) == value
|
||||
assert len(vban.strip[index].levels.prefader) == value
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
@ -137,8 +136,42 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_gainlayer_values(self, index, j, value):
|
||||
tests.strip[index].gainlayer[j].gain = value
|
||||
assert tests.strip[index].gainlayer[j].gain == value
|
||||
vban.strip[index].gainlayer[j].gain = value
|
||||
assert vban.strip[index].gainlayer[j].gain == value
|
||||
|
||||
""" strip tests, physical """
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Only test if logged into Potato version",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[
|
||||
(data.phys_in, "gainin", -8.6),
|
||||
(data.phys_in, "knee", 0.24),
|
||||
],
|
||||
)
|
||||
def test_it_sets_strip_comp_params(self, index, param, value):
|
||||
assert hasattr(vban.strip[index].comp, param)
|
||||
setattr(vban.strip[index].comp, param, value)
|
||||
# we can set but not get this value. Not in RT Packet.
|
||||
|
||||
@pytest.mark.skipif(
|
||||
data.name != "potato",
|
||||
reason="Only test if logged into Potato version",
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[
|
||||
(data.phys_in, "bpsidechain", 120),
|
||||
(data.phys_in, "hold", 3000),
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_gate_params(self, index, param, value):
|
||||
assert hasattr(vban.strip[index].gate, param)
|
||||
setattr(vban.strip[index].gate, param, value)
|
||||
# we can set but not get this value. Not in RT Packet.
|
||||
|
||||
""" strip tests, virtual """
|
||||
|
||||
@ -151,8 +184,8 @@ class TestSetAndGetFloatHigher:
|
||||
],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@ -161,15 +194,15 @@ class TestSetAndGetFloatHigher:
|
||||
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_float_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"index,value",
|
||||
[(data.phys_out, 8), (data.virt_out, 8)],
|
||||
)
|
||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||
assert len(tests.bus[index].levels.all) == value
|
||||
assert len(vban.bus[index].levels.all) == value
|
||||
|
||||
|
||||
@pytest.mark.parametrize("value", ["test0", "test1"])
|
||||
@ -183,8 +216,8 @@ class TestSetAndGetStringHigher:
|
||||
[(data.phys_in, "label"), (data.virt_in, "label")],
|
||||
)
|
||||
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
||||
setattr(tests.strip[index], param, value)
|
||||
assert getattr(tests.strip[index], param) == value
|
||||
setattr(vban.strip[index], param, value)
|
||||
assert getattr(vban.strip[index], param) == value
|
||||
|
||||
""" bus tests, physical and virtual """
|
||||
|
||||
@ -193,5 +226,5 @@ class TestSetAndGetStringHigher:
|
||||
[(data.phys_out, "label"), (data.virt_out, "label")],
|
||||
)
|
||||
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
||||
setattr(tests.bus[index], param, value)
|
||||
assert getattr(tests.bus[index], param) == value
|
||||
setattr(vban.bus[index], param, value)
|
||||
assert getattr(vban.bus[index], param) == value
|
||||
|
@ -1,9 +1,9 @@
|
||||
import time
|
||||
|
||||
import pytest
|
||||
from vban_cmd import kinds
|
||||
|
||||
from tests import data, tests
|
||||
from tests import data, vban
|
||||
from vban_cmd import kinds
|
||||
|
||||
|
||||
class TestPublicPacketLower:
|
||||
@ -12,7 +12,7 @@ class TestPublicPacketLower:
|
||||
"""Tests for a valid rt data packet"""
|
||||
|
||||
def test_it_gets_an_rt_data_packet(self):
|
||||
assert tests.public_packet.voicemeetertype in (
|
||||
assert vban.public_packet.voicemeetertype in (
|
||||
kind.name for kind in kinds.kinds_all
|
||||
)
|
||||
|
||||
@ -35,7 +35,7 @@ class TestSetRT:
|
||||
],
|
||||
)
|
||||
def test_it_sends_a_text_request(self, kls, index, param, value):
|
||||
tests._set_rt(f"{kls}[{index}]", param, value)
|
||||
vban._set_rt(f"{kls}[{index}]", param, value)
|
||||
time.sleep(0.02)
|
||||
target = getattr(tests, kls)[index]
|
||||
target = getattr(vban, kls)[index]
|
||||
assert getattr(target, param) == bool(value)
|
||||
|
@ -52,6 +52,23 @@ class Bus(IRemote):
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class BusEQ(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
BUSEQ_cls = type(
|
||||
f"BusEQ{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
**{param: channel_bool_prop(param) for param in ["on", "ab"]},
|
||||
},
|
||||
)
|
||||
return BUSEQ_cls(remote, index)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}].eq"
|
||||
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
@ -167,11 +184,10 @@ def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
f"{BUS_cls.__name__}{remote.kind}",
|
||||
(BUS_cls,),
|
||||
{
|
||||
"eq": BusEQ.make(remote, i),
|
||||
"levels": BusLevel(remote, i),
|
||||
"mode": BUSMODEMIXIN_cls(remote, i),
|
||||
**{param: channel_bool_prop(param) for param in ["mute", "mono"]},
|
||||
"eq": channel_bool_prop("eq.On"),
|
||||
"eq_ab": channel_bool_prop("eq.ab"),
|
||||
"label": channel_label_prop(),
|
||||
},
|
||||
)(remote, i)
|
||||
|
@ -1,5 +1,5 @@
|
||||
from .iremote import IRemote
|
||||
from .meta import action_prop
|
||||
from .meta import action_fn
|
||||
|
||||
|
||||
class Command(IRemote):
|
||||
@ -21,10 +21,9 @@ class Command(IRemote):
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
param: action_prop(param)
|
||||
for param in ["show", "shutdown", "restart"]
|
||||
param: action_fn(param) for param in ["show", "shutdown", "restart"]
|
||||
},
|
||||
"hide": action_prop("show", val=0),
|
||||
"hide": action_fn("show", val=0),
|
||||
},
|
||||
)
|
||||
return CMD_cls(remote)
|
||||
|
@ -2,6 +2,8 @@ import itertools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
from .error import VBANCMDError
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
@ -9,6 +11,8 @@ except ModuleNotFoundError:
|
||||
|
||||
from .kinds import request_kind_map as kindmap
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TOMLStrBuilder:
|
||||
"""builds a config profile, as a string, for the toml parser"""
|
||||
@ -32,10 +36,18 @@ class TOMLStrBuilder:
|
||||
+ [f"B{i} = false" for i in range(1, self.kind.virt_out + 1)]
|
||||
)
|
||||
self.phys_strip_params = self.virt_strip_params + [
|
||||
"comp = 0.0",
|
||||
"gate = 0.0",
|
||||
"comp.knob = 0.0",
|
||||
"gate.knob = 0.0",
|
||||
"denoiser.knob = 0.0",
|
||||
"eq.on = false",
|
||||
]
|
||||
self.bus_float = ["gain = 0.0"]
|
||||
self.bus_params = [
|
||||
"mono = false",
|
||||
"eq.on = false",
|
||||
"mute = false",
|
||||
"gain = 0.0",
|
||||
]
|
||||
self.bus_bool = ["mono = false", "eq = false", "mute = false"]
|
||||
|
||||
if profile == "reset":
|
||||
self.reset_config()
|
||||
@ -66,7 +78,7 @@ class TOMLStrBuilder:
|
||||
else self.virt_strip_params
|
||||
)
|
||||
case "bus":
|
||||
toml_str += ("\n").join(self.bus_bool)
|
||||
toml_str += ("\n").join(self.bus_params)
|
||||
case _:
|
||||
pass
|
||||
return toml_str + "\n"
|
||||
@ -119,10 +131,9 @@ class Loader(metaclass=SingletonType):
|
||||
loads data into memory if not found
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("config.Loader")
|
||||
|
||||
def __init__(self, kind):
|
||||
self._kind = kind
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._configs = dict()
|
||||
self.defaults(kind)
|
||||
self.parser = None
|
||||
@ -166,16 +177,16 @@ def loader(kind):
|
||||
|
||||
returns configs loaded into memory
|
||||
"""
|
||||
logger = logging.getLogger("config.loader")
|
||||
logger_loader = logger.getChild("loader")
|
||||
loader = Loader(kind)
|
||||
|
||||
for path in (
|
||||
Path.cwd() / "configs" / kind.name,
|
||||
Path(__file__).parent / "configs" / kind.name,
|
||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||
Path.home() / ".config" / "vban-cmd" / kind.name,
|
||||
Path.home() / "Documents" / "Voicemeeter" / "configs" / kind.name,
|
||||
):
|
||||
if path.is_dir():
|
||||
logger.info(f"Checking [{path}] for TOML config files:")
|
||||
logger_loader.info(f"Checking [{path}] for TOML config files:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
if loader.parse(identifier, file):
|
||||
@ -191,6 +202,6 @@ def request_config(kind_id: str):
|
||||
"""
|
||||
try:
|
||||
configs = loader(kindmap(kind_id))
|
||||
except KeyError as e:
|
||||
print(f"Unknown Voicemeeter kind '{kind_id}'")
|
||||
except KeyError:
|
||||
raise VBANCMDError(f"Unknown Voicemeeter kind {kind_id}")
|
||||
return configs
|
||||
|
@ -1,4 +1,6 @@
|
||||
class VBANCMDError(Exception):
|
||||
"""general errors"""
|
||||
"""Exception raised when general errors occur"""
|
||||
|
||||
pass
|
||||
|
||||
class VBANCMDConnectionError(Exception):
|
||||
"""Exception raised when connection/timeout errors occur"""
|
||||
|
@ -1,14 +1,15 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Event:
|
||||
"""Keeps track of event subscriptions"""
|
||||
|
||||
logger = logging.getLogger("event.event")
|
||||
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
def info(self, msg=None):
|
||||
info = (f"{msg} events",) if msg else ()
|
||||
|
@ -7,11 +7,14 @@ from typing import Iterable, NoReturn
|
||||
from .bus import request_bus_obj as bus
|
||||
from .command import Command
|
||||
from .config import request_config as configs
|
||||
from .error import VBANCMDError
|
||||
from .kinds import KindMapClass
|
||||
from .kinds import request_kind_map as kindmap
|
||||
from .strip import request_strip_obj as strip
|
||||
from .vbancmd import VbanCmd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FactoryBuilder:
|
||||
"""
|
||||
@ -20,7 +23,6 @@ class FactoryBuilder:
|
||||
Separates construction from representation.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("vbancmd.factorybuilder")
|
||||
BuilderProgress = IntEnum("BuilderProgress", "strip bus command", start=0)
|
||||
|
||||
def __init__(self, factory, kind: KindMapClass):
|
||||
@ -31,6 +33,7 @@ class FactoryBuilder:
|
||||
f"Finished building buses for {self._factory}",
|
||||
f"Finished building commands for {self._factory}",
|
||||
)
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
def _pinfo(self, name: str) -> NoReturn:
|
||||
"""prints progress status for each step"""
|
||||
@ -60,9 +63,6 @@ class FactoryBase(VbanCmd):
|
||||
"""Base class for factories, subclasses VbanCmd."""
|
||||
|
||||
def __init__(self, kind_id: str, **kwargs):
|
||||
defaultsubs = {"pdirty": True, "ldirty": False}
|
||||
if "subs" in kwargs:
|
||||
defaultsubs = defaultsubs | kwargs.pop("subs")
|
||||
defaultkwargs = {
|
||||
"ip": None,
|
||||
"port": 6980,
|
||||
@ -70,9 +70,13 @@ class FactoryBase(VbanCmd):
|
||||
"bps": 0,
|
||||
"channel": 0,
|
||||
"ratelimit": 0.01,
|
||||
"timeout": 5,
|
||||
"sync": False,
|
||||
"subs": defaultsubs,
|
||||
"pdirty": False,
|
||||
"ldirty": False,
|
||||
}
|
||||
if "subs" in kwargs:
|
||||
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
||||
kwargs = defaultkwargs | kwargs
|
||||
self.kind = kindmap(kind_id)
|
||||
super().__init__(**kwargs)
|
||||
@ -188,9 +192,12 @@ def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
||||
|
||||
Returns a reference to a VbanCmd class of a kind
|
||||
"""
|
||||
logger_entry = logger.getChild("factory.request_vbancmd_obj")
|
||||
|
||||
VBANCMD_obj = None
|
||||
try:
|
||||
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
||||
except (ValueError, TypeError) as e:
|
||||
raise SystemExit(e)
|
||||
logger_entry.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDError(str(e)) from e
|
||||
return VBANCMD_obj
|
||||
|
@ -1,7 +1,10 @@
|
||||
import logging
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class Modes:
|
||||
@ -26,9 +29,9 @@ class Modes:
|
||||
|
||||
_mask: hex = 0x000000F0
|
||||
|
||||
_eq_on: hex = 0x00000100
|
||||
_on: hex = 0x00000100 # eq.on
|
||||
_cross: hex = 0x00000200
|
||||
_eq_ab: hex = 0x00000800
|
||||
_ab: hex = 0x00000800 # eq.ab
|
||||
|
||||
_busa: hex = 0x00001000
|
||||
_busa1: hex = 0x00001000
|
||||
@ -85,10 +88,12 @@ class IRemote(metaclass=ABCMeta):
|
||||
def __init__(self, remote, index=None):
|
||||
self._remote = remote
|
||||
self.index = index
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._modes = Modes()
|
||||
|
||||
def getter(self, param):
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f"getter: {cmd}")
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd)
|
||||
if self._remote.sync:
|
||||
@ -96,7 +101,14 @@ class IRemote(metaclass=ABCMeta):
|
||||
|
||||
def setter(self, param, val):
|
||||
"""Sends a string request RT packet."""
|
||||
self._remote._set_rt(f"{self.identifier}", param, val)
|
||||
self.logger.debug(f"setter: {self._cmd(param)}={val}")
|
||||
self._remote._set_rt(self.identifier, param, val)
|
||||
|
||||
def _cmd(self, param):
|
||||
cmd = (self.identifier,)
|
||||
if param:
|
||||
cmd += (f".{param}",)
|
||||
return "".join(cmd)
|
||||
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
@ -113,20 +125,26 @@ class IRemote(metaclass=ABCMeta):
|
||||
def fget(attr, val):
|
||||
if attr == "mode":
|
||||
return (f"mode.{val}", 1)
|
||||
elif attr == "knob":
|
||||
return ("", val)
|
||||
return (attr, val)
|
||||
|
||||
script = str()
|
||||
for attr, val in data.items():
|
||||
if hasattr(self, attr):
|
||||
if not isinstance(val, dict):
|
||||
if attr in dir(self): # avoid calling getattr (with hasattr)
|
||||
attr, val = fget(attr, val)
|
||||
if isinstance(val, bool):
|
||||
val = 1 if val else 0
|
||||
|
||||
self._remote.cache[f"{self.identifier}.{attr}"] = val
|
||||
script += f"{self.identifier}.{attr}={val};"
|
||||
|
||||
self._remote.sendtext(script)
|
||||
self._remote.cache[self._cmd(attr)] = val
|
||||
self._remote._script += f"{self._cmd(attr)}={val};"
|
||||
else:
|
||||
target = getattr(self, attr)
|
||||
target.apply(val)
|
||||
return self
|
||||
|
||||
def then_wait(self):
|
||||
self.logger.debug(self._remote._script)
|
||||
self._remote.sendtext(self._remote._script)
|
||||
self._remote._script = str()
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
from .error import VBANCMDError
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
@ -97,7 +99,7 @@ def request_kind_map(kind_id):
|
||||
try:
|
||||
KIND_obj = kind_factory(kind_id)
|
||||
except ValueError as e:
|
||||
print(e)
|
||||
raise VBANCMDError(str(e)) from e
|
||||
return KIND_obj
|
||||
|
||||
|
||||
|
@ -16,7 +16,7 @@ def channel_bool_prop(param):
|
||||
)[self.index],
|
||||
"little",
|
||||
)
|
||||
& getattr(self._modes, f'_{param.replace(".", "_").lower()}')
|
||||
& getattr(self._modes, f"_{param.lower()}")
|
||||
== 0
|
||||
)
|
||||
|
||||
@ -91,8 +91,8 @@ def bus_mode_prop(param):
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def action_prop(param, val=1):
|
||||
"""A param that performs an action"""
|
||||
def action_fn(param, val=1):
|
||||
"""A function that performs an action"""
|
||||
|
||||
def fdo(self):
|
||||
self.setter(param, val)
|
||||
|
@ -1,6 +1,6 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
from .kinds import KindMapClass
|
||||
from .util import comp
|
||||
|
||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||
@ -13,11 +13,29 @@ HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
||||
class VbanRtPacket:
|
||||
"""Represents the body of a VBAN RT data packet"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
self._strip_level = self._generate_levels(self._inputLeveldB100)
|
||||
self._bus_level = self._generate_levels(self._outputLeveldB100)
|
||||
_kind: KindMapClass
|
||||
_voicemeeterType: bytes
|
||||
_reserved: bytes
|
||||
_buffersize: bytes
|
||||
_voicemeeterVersion: bytes
|
||||
_optionBits: bytes
|
||||
_samplerate: bytes
|
||||
_inputLeveldB100: bytes
|
||||
_outputLeveldB100: bytes
|
||||
_TransportBit: bytes
|
||||
_stripState: bytes
|
||||
_busState: bytes
|
||||
_stripGaindB100Layer1: bytes
|
||||
_stripGaindB100Layer2: bytes
|
||||
_stripGaindB100Layer3: bytes
|
||||
_stripGaindB100Layer4: bytes
|
||||
_stripGaindB100Layer5: bytes
|
||||
_stripGaindB100Layer6: bytes
|
||||
_stripGaindB100Layer7: bytes
|
||||
_stripGaindB100Layer8: bytes
|
||||
_busGaindB100: bytes
|
||||
_stripLabelUTF8c60: bytes
|
||||
_busLabelUTF8c60: bytes
|
||||
|
||||
def _generate_levels(self, levelarray) -> tuple:
|
||||
return tuple(
|
||||
@ -25,6 +43,14 @@ class VbanRtPacket:
|
||||
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"""
|
||||
|
||||
@ -46,8 +72,8 @@ class VbanRtPacket:
|
||||
|
||||
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_level)),
|
||||
tuple(not val for val in comp(bus_cache, self._bus_level)),
|
||||
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))
|
||||
|
||||
@ -77,12 +103,12 @@ class VbanRtPacket:
|
||||
@property
|
||||
def inputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all inputs for a kind"""
|
||||
return self._strip_level[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
|
||||
return self.strip_levels[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
|
||||
|
||||
@property
|
||||
def outputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all outputs for a kind"""
|
||||
return self._bus_level[0 : 8 * self._kind.num_bus]
|
||||
return self.bus_levels[0 : 8 * self._kind.num_bus]
|
||||
|
||||
@property
|
||||
def stripstate(self) -> tuple:
|
||||
|
@ -51,25 +51,22 @@ class Strip(IRemote):
|
||||
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, remote, index):
|
||||
return type(
|
||||
f"PhysicalStrip{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
"comp": StripComp(remote, index),
|
||||
"gate": StripGate(remote, index),
|
||||
"denoiser": StripDenoiser(remote, index),
|
||||
"eq": StripEQ(remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@property
|
||||
def comp(self) -> float:
|
||||
return
|
||||
|
||||
@comp.setter
|
||||
def comp(self, val: float):
|
||||
self.setter("Comp", val)
|
||||
|
||||
@property
|
||||
def gate(self) -> float:
|
||||
return
|
||||
|
||||
@gate.setter
|
||||
def gate(self, val: float):
|
||||
self.setter("gate", val)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return
|
||||
@ -79,6 +76,182 @@ class PhysicalStrip(Strip):
|
||||
return
|
||||
|
||||
|
||||
class StripComp(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}].comp"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
@property
|
||||
def gainin(self) -> float:
|
||||
return
|
||||
|
||||
@gainin.setter
|
||||
def gainin(self, val: float):
|
||||
self.setter("GainIn", val)
|
||||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
return
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, val: float):
|
||||
self.setter("Ratio", val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter("Threshold", val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter("Attack", val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
self.setter("Release", val)
|
||||
|
||||
@property
|
||||
def knee(self) -> float:
|
||||
return
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: float):
|
||||
self.setter("Knee", val)
|
||||
|
||||
@property
|
||||
def gainout(self) -> float:
|
||||
return
|
||||
|
||||
@gainout.setter
|
||||
def gainout(self, val: float):
|
||||
self.setter("GainOut", val)
|
||||
|
||||
@property
|
||||
def makeup(self) -> bool:
|
||||
return
|
||||
|
||||
@makeup.setter
|
||||
def makeup(self, val: bool):
|
||||
self.setter("makeup", 1 if val else 0)
|
||||
|
||||
|
||||
class StripGate(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}].gate"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
self.setter("Threshold", val)
|
||||
|
||||
@property
|
||||
def damping(self) -> float:
|
||||
return
|
||||
|
||||
@damping.setter
|
||||
def damping(self, val: float):
|
||||
self.setter("Damping", val)
|
||||
|
||||
@property
|
||||
def bpsidechain(self) -> int:
|
||||
return
|
||||
|
||||
@bpsidechain.setter
|
||||
def bpsidechain(self, val: int):
|
||||
self.setter("BPSidechain", val)
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
self.setter("Attack", val)
|
||||
|
||||
@property
|
||||
def hold(self) -> float:
|
||||
return
|
||||
|
||||
@hold.setter
|
||||
def hold(self, val: float):
|
||||
self.setter("Hold", val)
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
self.setter("Release", val)
|
||||
|
||||
|
||||
class StripDenoiser(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}].denoiser"
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
self.setter("", val)
|
||||
|
||||
|
||||
class StripEQ(IRemote):
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}].eq"
|
||||
|
||||
@property
|
||||
def on(self):
|
||||
return
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter("on", 1 if val else 0)
|
||||
|
||||
@property
|
||||
def ab(self):
|
||||
return
|
||||
|
||||
@ab.setter
|
||||
def ab(self, val: bool):
|
||||
self.setter("ab", 1 if val else 0)
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
@ -232,7 +405,7 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
||||
|
||||
Returns a physical or virtual strip subclass
|
||||
"""
|
||||
STRIP_cls = PhysicalStrip if is_phys_strip else VirtualStrip
|
||||
STRIP_cls = PhysicalStrip.make(remote, i) if is_phys_strip else VirtualStrip
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||
|
||||
|
@ -1,15 +1,14 @@
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subject:
|
||||
"""Adds support for observers"""
|
||||
|
||||
logger = logging.getLogger("subject.subject")
|
||||
|
||||
def __init__(self):
|
||||
"""list of current observers"""
|
||||
"""Adds support for observers and callbacks"""
|
||||
|
||||
self._observers = list()
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
|
||||
@property
|
||||
def observers(self) -> list:
|
||||
@ -17,38 +16,57 @@ class Subject:
|
||||
|
||||
return self._observers
|
||||
|
||||
def notify(self, modifier=None):
|
||||
def notify(self, event):
|
||||
"""run callbacks on update"""
|
||||
|
||||
[o.on_update(modifier) for o in self._observers]
|
||||
for o in self._observers:
|
||||
if hasattr(o, "on_update"):
|
||||
o.on_update(event)
|
||||
else:
|
||||
if o.__name__ == f"on_{event}":
|
||||
o()
|
||||
|
||||
def add(self, observer):
|
||||
"""adds an observer to _observers"""
|
||||
"""adds an observer to observers"""
|
||||
|
||||
try:
|
||||
iterator = iter(observer)
|
||||
for o in iterator:
|
||||
if o not in self._observers:
|
||||
self._observers.append(o)
|
||||
self.logger.info(f"{o} added to event observers")
|
||||
else:
|
||||
self.logger.error(f"Failed to add {o} to event observers")
|
||||
except TypeError:
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
self.logger.info(f"{type(observer).__name__} added to event observers")
|
||||
self.logger.info(f"{observer} added to event observers")
|
||||
else:
|
||||
self.logger.error(
|
||||
f"Failed to add {type(observer).__name__} to event observers"
|
||||
)
|
||||
self.logger.error(f"Failed to add {observer} to event observers")
|
||||
|
||||
register = add
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from _observers"""
|
||||
"""removes an observer from observers"""
|
||||
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
self.logger.info(f"{type(observer).__name__} removed from event observers")
|
||||
iterator = iter(observer)
|
||||
for o in iterator:
|
||||
try:
|
||||
self._observers.remove(o)
|
||||
self.logger.info(f"{o} removed from event observers")
|
||||
except ValueError:
|
||||
self.logger.error(
|
||||
f"Failed to remove {type(observer).__name__} from event observers"
|
||||
)
|
||||
self.logger.error(f"Failed to remove {o} from event observers")
|
||||
except TypeError:
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
self.logger.info(f"{observer} removed from event observers")
|
||||
except ValueError:
|
||||
self.logger.error(f"Failed to remove {observer} from event observers")
|
||||
|
||||
deregister = remove
|
||||
|
||||
def clear(self):
|
||||
"""clears the _observers list"""
|
||||
"""clears the observers list"""
|
||||
|
||||
self._observers.clear()
|
||||
|
@ -3,18 +3,17 @@ import socket
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
from .error import VBANCMDError
|
||||
from .event import Event
|
||||
from .packet import RequestHeader
|
||||
from .subject import Subject
|
||||
from .util import Socket, script
|
||||
from .worker import Subscriber, Updater
|
||||
from .worker import Producer, Subscriber, Updater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VbanCmd(metaclass=ABCMeta):
|
||||
@ -28,15 +27,14 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
1000000, 1500000, 2000000, 3000000,
|
||||
]
|
||||
# fmt: on
|
||||
logger = logging.getLogger("vbancmd.vbancmd")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.event = Event({k: kwargs.pop(k) for k in ("pdirty", "ldirty")})
|
||||
if not kwargs["ip"]:
|
||||
kwargs |= self._conn_from_toml()
|
||||
for attr, val in kwargs.items():
|
||||
setattr(self, attr, val)
|
||||
if self.ip is None:
|
||||
conn = self._conn_from_toml()
|
||||
for attr, val in conn.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
self.packet_request = RequestHeader(
|
||||
name=self.streamname,
|
||||
@ -46,9 +44,8 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
self.socks = tuple(
|
||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
||||
)
|
||||
self.subject = Subject()
|
||||
self.subject = self.observer = Subject()
|
||||
self.cache = {}
|
||||
self.event = Event(self.subs)
|
||||
self._pdirty = False
|
||||
self._ldirty = False
|
||||
|
||||
@ -57,11 +54,31 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
|
||||
def _conn_from_toml(self) -> str:
|
||||
filepath = Path.cwd() / "vban.toml"
|
||||
def _conn_from_toml(self) -> dict:
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
def get_filepath():
|
||||
filepaths = [
|
||||
Path.cwd() / "vban.toml",
|
||||
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
||||
Path.home() / "Documents" / "Voicemeeter" / "vban.toml",
|
||||
]
|
||||
for filepath in filepaths:
|
||||
if filepath.exists():
|
||||
return filepath
|
||||
|
||||
if filepath := get_filepath():
|
||||
with open(filepath, "rb") as f:
|
||||
conn = tomllib.load(f)
|
||||
assert (
|
||||
"ip" in conn["connection"]
|
||||
), "please provide ip, by kwarg or config"
|
||||
return conn["connection"]
|
||||
else:
|
||||
raise VBANCMDError("no ip provided and no vban.toml located.")
|
||||
|
||||
def __enter__(self):
|
||||
self.login()
|
||||
@ -75,8 +92,11 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
self.subscriber = Subscriber(self)
|
||||
self.subscriber.start()
|
||||
|
||||
self.updater = Updater(self)
|
||||
queue = Queue()
|
||||
self.updater = Updater(self, queue)
|
||||
self.updater.start()
|
||||
self.producer = Producer(self, queue)
|
||||
self.producer.start()
|
||||
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
||||
|
||||
@ -87,20 +107,27 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
val: Optional[Union[int, float]] = None,
|
||||
):
|
||||
"""Sends a string request command over a network."""
|
||||
cmd = id_ if not param else f"{id_}.{param}={val};"
|
||||
cmd = f"{id_}={val};" if not param else f"{id_}.{param}={val};"
|
||||
self.socks[Socket.request].sendto(
|
||||
self.packet_request.header + cmd.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
count = int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
self.packet_request.framecounter = count.to_bytes(4, "little")
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
if param:
|
||||
self.cache[f"{id_}.{param}"] = val
|
||||
|
||||
@script
|
||||
def sendtext(self, cmd):
|
||||
"""Sends a multiple parameter string over a network."""
|
||||
self._set_rt(cmd)
|
||||
self.socks[Socket.request].sendto(
|
||||
self.packet_request.header + cmd.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
@property
|
||||
@ -157,6 +184,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
else:
|
||||
raise ValueError(obj)
|
||||
|
||||
self._script = str()
|
||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
||||
|
||||
def apply_config(self, name):
|
||||
@ -168,7 +196,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
except KeyError as e:
|
||||
except KeyError:
|
||||
self.logger.error(("\n").join(error_msg))
|
||||
|
||||
def logout(self):
|
||||
|
@ -4,60 +4,66 @@ import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .error import VBANCMDError
|
||||
from .error import VBANCMDConnectionError
|
||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
||||
from .util import Socket, comp
|
||||
from .util import Socket
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Subscriber(threading.Thread):
|
||||
"""fire a subscription packet every 10 seconds"""
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="subscriber", target=self.subscribe, daemon=True)
|
||||
super().__init__(name="subscriber", daemon=True)
|
||||
self._remote = remote
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.packet = SubscribeHeader()
|
||||
|
||||
def subscribe(self):
|
||||
def run(self):
|
||||
while self._remote.running:
|
||||
try:
|
||||
self._remote.socks[Socket.register].sendto(
|
||||
self.packet.header,
|
||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
||||
)
|
||||
count = int.from_bytes(self.packet.framecounter, "little") + 1
|
||||
self.packet.framecounter = count.to_bytes(4, "little")
|
||||
self.packet.framecounter = (
|
||||
int.from_bytes(self.packet.framecounter, "little") + 1
|
||||
).to_bytes(4, "little")
|
||||
time.sleep(10)
|
||||
except socket.gaierror:
|
||||
err_msg = f"Unable to resolve hostname {self._remote.ip}"
|
||||
print(err_msg)
|
||||
raise VBANCMDError(err_msg)
|
||||
except socket.gaierror as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDConnectionError(
|
||||
f"unable to resolve hostname {self._remote.ip}"
|
||||
) from e
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
"""
|
||||
continously updates the public packet
|
||||
class Producer(threading.Thread):
|
||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||
|
||||
notifies observers of event updates
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("worker.updater")
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="updater", target=self.update, daemon=True)
|
||||
def __init__(self, remote, queue):
|
||||
super().__init__(name="producer", daemon=True)
|
||||
self._remote = remote
|
||||
self._remote.socks[Socket.response].settimeout(5)
|
||||
self._remote.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||
)
|
||||
self.queue = queue
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.packet_expected = VbanRtPacketHeader()
|
||||
self._remote._public_packet = self._get_rt()
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
) = self._remote._get_levels(self._remote.public_packet)
|
||||
p_in, v_in = self._remote.kind.ins
|
||||
self._remote._strip_comp = [False] * (2 * p_in + 8 * v_in)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
||||
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
def fget():
|
||||
data = None
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
time.sleep(self._remote.DELAY)
|
||||
return data
|
||||
|
||||
return fget()
|
||||
|
||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||
try:
|
||||
@ -66,7 +72,6 @@ class Updater(threading.Thread):
|
||||
if len(data) > HEADER_SIZE:
|
||||
# check if packet is of type rt packet response
|
||||
if self.packet_expected.header == data[: HEADER_SIZE - 4]:
|
||||
self.logger.debug("valid packet received")
|
||||
return VbanRtPacket(
|
||||
_kind=self._remote.kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
@ -92,26 +97,14 @@ class Updater(threading.Thread):
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
except TimeoutError:
|
||||
err_msg = f"Unable to establish connection with {self._remote.ip}"
|
||||
print(err_msg)
|
||||
raise VBANCMDError(err_msg)
|
||||
except TimeoutError as e:
|
||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||
raise VBANCMDConnectionError(
|
||||
f"timeout waiting for RtPacket from {self._remote.ip}"
|
||||
) from e
|
||||
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
def fget():
|
||||
data = None
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
time.sleep(self._remote.DELAY)
|
||||
return data
|
||||
|
||||
return fget()
|
||||
|
||||
def update(self):
|
||||
def run(self):
|
||||
while self._remote.running:
|
||||
start = time.time()
|
||||
_pp = self._get_rt()
|
||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||
ldirty = _pp.ldirty(
|
||||
@ -119,24 +112,63 @@ class Updater(threading.Thread):
|
||||
)
|
||||
|
||||
if pdirty or ldirty:
|
||||
self.logger.debug("dirty state, updating public packet")
|
||||
self._remote._public_packet = _pp
|
||||
self._remote._pdirty = pdirty
|
||||
self._remote._ldirty = ldirty
|
||||
|
||||
if self._remote.event.pdirty and self._remote.pdirty:
|
||||
self._remote.subject.notify("pdirty")
|
||||
if self._remote.event.ldirty and self._remote.ldirty:
|
||||
self._remote._strip_comp, self._remote._bus_comp = (
|
||||
_pp._strip_comp,
|
||||
_pp._bus_comp,
|
||||
)
|
||||
self._remote.cache["strip_level"], self._remote.cache["bus_level"] = (
|
||||
_pp.inputlevels,
|
||||
_pp.outputlevels,
|
||||
)
|
||||
self._remote.subject.notify("ldirty")
|
||||
if self._remote.event.pdirty:
|
||||
self.queue.put("pdirty")
|
||||
if self._remote.event.ldirty:
|
||||
self.queue.put("ldirty")
|
||||
time.sleep(self._remote.ratelimit)
|
||||
self.logger.debug(f"terminating {self.name} thread")
|
||||
self.queue.put(None)
|
||||
|
||||
elapsed = time.time() - start
|
||||
if self._remote.ratelimit - elapsed > 0:
|
||||
time.sleep(self._remote.ratelimit - elapsed)
|
||||
|
||||
class Updater(threading.Thread):
|
||||
"""
|
||||
continously updates the public packet
|
||||
|
||||
notifies observers of event updates
|
||||
"""
|
||||
|
||||
def __init__(self, remote, queue):
|
||||
super().__init__(name="updater", daemon=True)
|
||||
self._remote = remote
|
||||
self.queue = queue
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
||||
self._remote.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||
)
|
||||
p_in, v_in = self._remote.kind.ins
|
||||
self._remote._strip_comp = [False] * (2 * p_in + 8 * v_in)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Continously update observers of dirty states.
|
||||
|
||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||
"""
|
||||
while True:
|
||||
event = self.queue.get()
|
||||
if event is None:
|
||||
self.logger.debug(f"terminating {self.name} thread")
|
||||
break
|
||||
|
||||
if event == "pdirty" and self._remote.pdirty:
|
||||
self._remote.subject.notify(event)
|
||||
elif event == "ldirty" and self._remote.ldirty:
|
||||
self._remote._strip_comp, self._remote._bus_comp = (
|
||||
self._remote._public_packet._strip_comp,
|
||||
self._remote._public_packet._bus_comp,
|
||||
)
|
||||
(
|
||||
self._remote.cache["strip_level"],
|
||||
self._remote.cache["bus_level"],
|
||||
) = (
|
||||
self._remote._public_packet.inputlevels,
|
||||
self._remote._public_packet.outputlevels,
|
||||
)
|
||||
self._remote.subject.notify(event)
|
||||
|
Loading…
x
Reference in New Issue
Block a user