mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-18 13:03:31 +00:00
Compare commits
48 Commits
main
...
f6218d2032
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f6218d2032 | ||
|
|
4aacc60857 | ||
|
|
8f9ac47d02 | ||
|
|
90e994c193 | ||
|
|
44cd13aa48 | ||
|
|
87eb61170e | ||
|
|
01c99d5b31 | ||
|
|
3144a95e07 | ||
|
|
1833b28c8d | ||
|
|
ee3a871d23 | ||
|
|
197f81aa73 | ||
|
|
362873c5be | ||
|
|
c86f7971b0 | ||
|
|
bac60e5ed3 | ||
|
|
692acc8dd0 | ||
|
|
d57269f147 | ||
|
|
be69d905c4 | ||
|
|
5ceb8f775a | ||
|
|
e0f4aab257 | ||
|
|
4ee37f54c5 | ||
|
|
550df917fb | ||
|
|
2f82e0b1fc | ||
|
|
0c60fe3d5e | ||
|
|
243a43ac22 | ||
|
|
49354d6d55 | ||
|
|
5c9ac4d78f | ||
|
|
02b21b6989 | ||
|
|
4659cf7cdb | ||
|
|
8663aab2ce | ||
|
|
a029011012 | ||
|
|
bfa1a718f9 | ||
|
|
2048a807d1 | ||
|
|
566bff3ced | ||
|
|
70dbee6f02 | ||
|
|
c14196fc31 | ||
|
|
c28398c5f6 | ||
|
|
5177c2d297 | ||
|
|
23bc15e437 | ||
|
|
db96872965 | ||
|
|
1169435104 | ||
|
|
f46abedf12 | ||
|
|
733fab45b4 | ||
|
|
444f95a9d6 | ||
|
|
14e538dca6 | ||
|
|
af5e81c339 | ||
|
|
aadfbd3925 | ||
|
|
4ef3d1f225 | ||
|
|
aea2be624e |
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
# quick test
|
||||
quick.py
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
@@ -153,3 +150,10 @@ cython_debug/
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# quick test
|
||||
quick.py
|
||||
|
||||
#config
|
||||
config.toml
|
||||
vban.toml
|
||||
62
CHANGELOG.md
62
CHANGELOG.md
@@ -11,6 +11,68 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [1.8.0]
|
||||
|
||||
### Added
|
||||
|
||||
- Connection section to README.
|
||||
|
||||
### Changed
|
||||
|
||||
- now using clear_dirty() when sync enabled.
|
||||
|
||||
### Fixed
|
||||
|
||||
- bug in set_rt() where multiple commands sent in single request packet.
|
||||
- bug in apply where index was sent twice.
|
||||
|
||||
## [1.7.0]
|
||||
|
||||
### Added
|
||||
|
||||
- ability to read conn info from vban.toml config
|
||||
|
||||
### Changed
|
||||
|
||||
- assume a vban.toml in examples. README's modified.
|
||||
|
||||
## [1.6.0] - 2022-10-06
|
||||
|
||||
### Added
|
||||
|
||||
- fadeto(), fadeby() methods added to strip/bus classes.
|
||||
- OBS example added.
|
||||
|
||||
### Changed
|
||||
|
||||
- Event class add/remove now accept iterables.
|
||||
- property setters added to Event class.
|
||||
- ldirty logic moved into VbanRtPacket class.
|
||||
- in util, threshold a level is considered dirty moved to 7200 (-72.0)
|
||||
- now print bus levels in observer example.
|
||||
|
||||
### Fixed
|
||||
|
||||
- initialize comps in updater thread. fixes bug when switching to a kind before any level updates
|
||||
|
||||
## [1.5.0] - 2022-09-28
|
||||
|
||||
### Changed
|
||||
|
||||
- Logging module used in place of print statements across the interface.
|
||||
- base error name changed (VBANCMDError)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Timeout and raise connection error when socket connection fails.
|
||||
- Bug in observer example
|
||||
|
||||
## [1.4.0] - 2022-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- tomli/tomllib compatibility layer to support python 3.10
|
||||
|
||||
## [1.3.0] - 2022-08-02
|
||||
|
||||
### Added
|
||||
|
||||
172
README.md
172
README.md
@@ -25,24 +25,35 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||
## Requirements
|
||||
|
||||
- [Voicemeeter](https://voicemeeter.com/)
|
||||
- Python 3.11 or greater
|
||||
- Python 3.10 or greater
|
||||
|
||||
## Installation
|
||||
|
||||
### `Pip`
|
||||
|
||||
Install vban-cmd package from your console
|
||||
|
||||
`pip install vban-cmd`
|
||||
|
||||
## `Use`
|
||||
|
||||
#### Connection
|
||||
|
||||
Load VBAN connection info from toml config. A valid `vban.toml` might look like this:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
It should be placed next to your `__main__.py` file.
|
||||
|
||||
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
||||
|
||||
#### `__main__.py`
|
||||
|
||||
Simplest use case, use a context manager to request a VbanCmd class of a kind.
|
||||
|
||||
Login and logout are handled for you in this scenario.
|
||||
|
||||
#### `__main__.py`
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
|
||||
@@ -59,17 +70,21 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
||||
)
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
kind_id = "banana"
|
||||
|
||||
with vban_cmd.api(
|
||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
) as vban:
|
||||
do = ManyThings(vban)
|
||||
do.things()
|
||||
do.other_things()
|
||||
@@ -84,13 +99,6 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||
@@ -124,7 +132,24 @@ example:
|
||||
|
||||
```python
|
||||
vban.strip[3].gain = 3.7
|
||||
print(strip[0].label)
|
||||
print(vban.strip[0].label)
|
||||
```
|
||||
|
||||
The following methods are available.
|
||||
|
||||
- `appgain(name, value)`: string, float, from 0.0 to 1.0
|
||||
|
||||
Set the gain in db by value for the app matching name.
|
||||
|
||||
- `appmute(name, value)`: string, bool
|
||||
|
||||
Set mute state as value for the app matching name.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[5].appmute("Spotify", True)
|
||||
vban.strip[5].appgain("Spotify", 0.5)
|
||||
```
|
||||
|
||||
##### Gainlayers
|
||||
@@ -213,6 +238,22 @@ print(vban.bus[0].levels.all)
|
||||
|
||||
`levels.all` will return -200.0 if no audio detected.
|
||||
|
||||
### Strip | Bus
|
||||
|
||||
The following methods are available.
|
||||
|
||||
- `fadeto(amount, time)`: float, int
|
||||
- `fadeby(amount, time)`: float, int
|
||||
|
||||
Modify gain to or by the selected amount in db over a time interval in ms.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].fadeto(-10.3, 1000)
|
||||
vban.bus[3].fadeby(-5.6, 500)
|
||||
```
|
||||
|
||||
### Command
|
||||
|
||||
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:
|
||||
@@ -242,8 +283,8 @@ vban.command.showvbanchat = true
|
||||
```python
|
||||
vban.apply(
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-1": {"mute": True, "mode": "composite"},
|
||||
}
|
||||
)
|
||||
```
|
||||
@@ -252,7 +293,7 @@ Or for each class you may do:
|
||||
|
||||
```python
|
||||
vban.strip[0].apply(mute: true, gain: 3.2, A1: true)
|
||||
vban.vban.outstream[0].apply(on: true, name: 'streamname', bit: 24)
|
||||
vban.bus[0].apply(A1: true)
|
||||
```
|
||||
|
||||
## Config Files
|
||||
@@ -271,33 +312,9 @@ with vban_cmd.api('banana') as vban:
|
||||
|
||||
will load a config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||
|
||||
## `Base Module`
|
||||
## Events
|
||||
|
||||
### VbanCmd class
|
||||
|
||||
`vban_cmd.api(kind_id: str, **opts: dict)`
|
||||
|
||||
You may pass the following optional keyword arguments:
|
||||
|
||||
- `ip`: str, ip or hostname of remote machine
|
||||
- `streamname`: str, name of the stream to connect to.
|
||||
- `port`: int=6980, vban udp port of remote machine.
|
||||
- `subs`: dict={"pdirty": True, "ldirty": False}, controls which updates to listen for.
|
||||
- `pdirty`: parameter updates
|
||||
- `ldirty`: level updates
|
||||
|
||||
#### Event updates
|
||||
|
||||
To receive event updates you should do the following:
|
||||
|
||||
- register your app to receive updates using the `vban.subject.add(observer)` method, where observer is your app.
|
||||
- define an `on_update(subject)` callback function in your app. The value of subject may be checked for the type of update.
|
||||
|
||||
See `examples/observer` for a demonstration.
|
||||
|
||||
Level updates are considered high volume, by default they are NOT listened for.
|
||||
|
||||
Each of the update types may be enabled/disabled separately.
|
||||
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
||||
|
||||
example:
|
||||
|
||||
@@ -314,21 +331,72 @@ with vban_cmd.api('banana', **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
#### `vban.event`
|
||||
#### `vban.subject`
|
||||
|
||||
You may also add/remove event subscriptions as necessary with the Event class.
|
||||
Use the Subject class to register an app as event observer.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add`: registers an app as an event observer
|
||||
- `remove`: deregisters an app as an event observer
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.add("ldirty")
|
||||
# register an app to receive updates
|
||||
class App():
|
||||
def __init__(self, vban):
|
||||
vban.subject.add(self)
|
||||
...
|
||||
```
|
||||
|
||||
vban.event.remove("pdirty")
|
||||
#### `vban.event`
|
||||
|
||||
Use the event class to toggle updates as necessary.
|
||||
|
||||
The following properties are available:
|
||||
|
||||
- `pdirty`: boolean
|
||||
- `ldirty`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.ldirty = True
|
||||
|
||||
vban.event.pdirty = False
|
||||
```
|
||||
|
||||
Or add, remove a list of events.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add()`
|
||||
- `remove()`
|
||||
- `get()`
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.event.remove(["pdirty", "ldirty"])
|
||||
|
||||
# get a list of currently subscribed
|
||||
print(vban.event.get())
|
||||
```
|
||||
|
||||
## VbanCmd class
|
||||
|
||||
`vban_cmd.api(kind_id: str, **opts: dict)`
|
||||
|
||||
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
|
||||
|
||||
#### `vban.pdirty`
|
||||
|
||||
True iff a parameter has been changed.
|
||||
@@ -351,7 +419,7 @@ Returns a Voicemeeter rt data packet object. Designed to be used internally by t
|
||||
|
||||
### `Errors`
|
||||
|
||||
- `errors.VMCMDErrors`: Base VMCMD error class.
|
||||
- `errors.VBANCMDError`: Base VMCMD error class.
|
||||
|
||||
### `Tests`
|
||||
|
||||
|
||||
20
__main__.py
20
__main__.py
@@ -13,17 +13,21 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
||||
)
|
||||
self.vban.bus[3].gain = -6.3
|
||||
self.vban.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
kind_id = "banana"
|
||||
|
||||
with vban_cmd.api(
|
||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
||||
) as vban:
|
||||
do = ManyThings(vban)
|
||||
do.things()
|
||||
do.other_things()
|
||||
@@ -33,19 +37,9 @@ def main():
|
||||
{
|
||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||
"bus-2": {"mute": True},
|
||||
"button-0": {"state": True},
|
||||
"vban-in-0": {"on": True},
|
||||
"vban-out-1": {"name": "streamname"},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
51
examples/obs/README.md
Normal file
51
examples/obs/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
## Requirements
|
||||
|
||||
- [OBS Studio](https://obsproject.com/)
|
||||
- [OBS Python SDK for Websocket v5](https://github.com/aatikturk/obsws-python)
|
||||
|
||||
## About
|
||||
|
||||
Perhaps you have a streaming setup but you want to control OBS and Voicemeeter from a remote location with python installed.
|
||||
With the vban-cmd and obsws-python packages you may sync a distant Voicemeeter with a distant OBS over LAN.
|
||||
|
||||
## Configure
|
||||
|
||||
This script assumes the following:
|
||||
|
||||
- OBS Connection info in a valid `config.toml`:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
host = "gamepc.local"
|
||||
port = 4455
|
||||
password = "mystrongpass"
|
||||
```
|
||||
|
||||
- VBAN Connection info in a valid `vban.toml`:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
- Both configs should be placed next to `__main__.py`.
|
||||
|
||||
- Four OBS scenes named "START", "BRB", "END" and "LIVE".
|
||||
|
||||
## Use
|
||||
|
||||
Make sure you have established a working connection to OBS and the remote Voicemeeter.
|
||||
|
||||
Run the script, change OBS scenes and watch Voicemeeter parameters change.
|
||||
|
||||
Pressing `<Enter>` will exit.
|
||||
|
||||
## Notes
|
||||
|
||||
This script can be run from a Linux host since the vban-cmd interface relies on UDP packets and obsws-python runs over websockets.
|
||||
|
||||
You could for example, set this up to run in the background on a home server such as a Raspberry Pi.
|
||||
|
||||
It requires Python 3.10+.
|
||||
63
examples/obs/__main__.py
Normal file
63
examples/obs/__main__.py
Normal file
@@ -0,0 +1,63 @@
|
||||
import logging
|
||||
|
||||
import obsws_python as obs
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
self.client = obs.EventClient()
|
||||
self.client.callback.register(self.on_current_program_scene_changed)
|
||||
|
||||
def on_start(self):
|
||||
self.vban.strip[0].mute = True
|
||||
self.vban.strip[1].B1 = True
|
||||
self.vban.strip[2].B2 = True
|
||||
|
||||
def on_brb(self):
|
||||
self.vban.strip[7].fadeto(0, 500)
|
||||
self.vban.bus[0].mute = True
|
||||
|
||||
def on_end(self):
|
||||
self.vban.apply(
|
||||
{
|
||||
"strip-0": {"mute": True},
|
||||
"strip-1": {"mute": True, "B1": False},
|
||||
"strip-2": {"mute": True, "B1": False},
|
||||
}
|
||||
)
|
||||
|
||||
def on_live(self):
|
||||
self.vban.strip[0].mute = False
|
||||
self.vban.strip[7].fadeto(-6, 500)
|
||||
self.vban.strip[7].A3 = True
|
||||
|
||||
def on_current_program_scene_changed(self, data):
|
||||
def fget(scene):
|
||||
run = {
|
||||
"START": self.on_start,
|
||||
"BRB": self.on_brb,
|
||||
"END": self.on_end,
|
||||
"LIVE": self.on_live,
|
||||
}
|
||||
return run.get(scene)
|
||||
|
||||
scene = data.scene_name
|
||||
print(f"Switched to scene {scene}")
|
||||
if fn := fget(scene):
|
||||
fn()
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api("potato", sync=True) as vban:
|
||||
obs = Observer(vban)
|
||||
while cmd := input("<Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
examples/observer/README.md
Normal file
29
examples/observer/README.md
Normal file
@@ -0,0 +1,29 @@
|
||||
## About
|
||||
|
||||
Registers a class as an observer and defines a callback.
|
||||
|
||||
## Configure
|
||||
|
||||
The script assumes you have connection info saved in a config file named `vban.toml` placed next to `__main__.py`.
|
||||
|
||||
A valid `vban.toml` might look like this:
|
||||
|
||||
```toml
|
||||
[connection]
|
||||
ip = "gamepc.local"
|
||||
port = 6980
|
||||
streamname = "Command1"
|
||||
```
|
||||
|
||||
It should be placed next to `__main__.py`.
|
||||
|
||||
## Use
|
||||
|
||||
Make sure you have established a working VBAN connection.
|
||||
|
||||
Run the script, then:
|
||||
|
||||
- change GUI parameters to trigger pdirty
|
||||
- play audio through any bus to trigger ldirty
|
||||
|
||||
Pressing `<Enter>` will exit.
|
||||
@@ -1,35 +1,33 @@
|
||||
import logging
|
||||
|
||||
import vban_cmd
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vban):
|
||||
self.vban = vban
|
||||
# register your app as event observer
|
||||
self.vban.subject.add(self)
|
||||
# add level updates, since they are disabled by default.
|
||||
self.vm.event.add("ldirty")
|
||||
# enable level updates, since they are disabled by default.
|
||||
self.vban.event.ldirty = True
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, subject):
|
||||
if subject == "pdirty":
|
||||
print("pdirty!")
|
||||
elif subject == "ldirty":
|
||||
info = (
|
||||
f"[{self.vban.bus[0]} {self.vban.bus[0].levels.isdirty}]",
|
||||
f"[{self.vban.bus[1]} {self.vban.bus[1].levels.isdirty}]",
|
||||
f"[{self.vban.bus[2]} {self.vban.bus[2].levels.isdirty}]",
|
||||
f"[{self.vban.bus[3]} {self.vban.bus[3].levels.isdirty}]",
|
||||
f"[{self.vban.bus[4]} {self.vban.bus[4].levels.isdirty}]",
|
||||
f"[{self.vban.bus[5]} {self.vban.bus[5].levels.isdirty}]",
|
||||
f"[{self.vban.bus[6]} {self.vban.bus[6].levels.isdirty}]",
|
||||
f"[{self.vban.bus[7]} {self.vban.bus[7].levels.isdirty}]",
|
||||
)
|
||||
print(" ".join(info))
|
||||
for bus in self.vban.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
|
||||
|
||||
def main():
|
||||
with vban_cmd.api(kind_id, **opts) as vban:
|
||||
obs = Observer(vban)
|
||||
kind_id = "potato"
|
||||
|
||||
with vban_cmd.api(kind_id) as vban:
|
||||
Observer(vban)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
@@ -37,11 +35,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "potato"
|
||||
opts = {
|
||||
"ip": "<ip address>",
|
||||
"streamname": "Command1",
|
||||
"port": 6980,
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
125
poetry.lock
generated
125
poetry.lock
generated
@@ -1,11 +1,3 @@
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.1"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
@@ -22,7 +14,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.6.0"
|
||||
version = "22.8.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -33,6 +25,7 @@ click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
@@ -102,11 +95,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
@@ -129,8 +122,8 @@ optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
testing = ["pytest-benchmark", "pytest"]
|
||||
dev = ["tox", "pre-commit"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
@@ -153,14 +146,13 @@ diagrams = ["railroad-diagrams", "jinja2"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.2"
|
||||
version = "7.1.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
@@ -198,97 +190,30 @@ pytest = ">=3.6"
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = []
|
||||
attrs = []
|
||||
black = [
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
|
||||
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
|
||||
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
|
||||
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
|
||||
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
|
||||
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
|
||||
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
|
||||
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
|
||||
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
|
||||
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
|
||||
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
|
||||
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
|
||||
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
|
||||
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
|
||||
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
|
||||
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
|
||||
]
|
||||
pytest-randomly = [
|
||||
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
|
||||
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
|
||||
]
|
||||
black = []
|
||||
click = []
|
||||
colorama = []
|
||||
iniconfig = []
|
||||
isort = []
|
||||
mypy-extensions = []
|
||||
packaging = []
|
||||
pathspec = []
|
||||
platformdirs = []
|
||||
pluggy = []
|
||||
py = []
|
||||
pyparsing = []
|
||||
pytest = []
|
||||
pytest-randomly = []
|
||||
pytest-repeat = []
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
tomli = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "vban-cmd"
|
||||
version = "1.3.2"
|
||||
version = "1.8.1"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
@@ -8,7 +8,8 @@ readme = "README.md"
|
||||
repository = "https://github.com/onyx-and-iris/vban-cmd-python"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
python = "^3.10"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
@@ -21,3 +22,7 @@ isort = "^5.10.1"
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
obs = "scripts:ex_obs"
|
||||
observer = "scripts:ex_observer"
|
||||
|
||||
12
scripts.py
Normal file
12
scripts.py
Normal file
@@ -0,0 +1,12 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_obs():
|
||||
path = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
path = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
@@ -32,22 +32,25 @@ class Bus(IRemote):
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = self.public_packet.busgain[self.index]
|
||||
if val < 10000:
|
||||
return -val
|
||||
elif val == ((1 << 16) - 1):
|
||||
return 0
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter("gain")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
def __str__(self):
|
||||
@@ -79,9 +82,19 @@ class BusLevel(IRemote):
|
||||
def getter(self):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||
|
||||
if self._remote.running and self._remote.event.ldirty:
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
||||
)
|
||||
return tuple(
|
||||
round(-i * 0.01, 1)
|
||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packet)[1][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from .error import VMCMDErrors
|
||||
from .iremote import IRemote
|
||||
from .meta import action_prop
|
||||
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import itertools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
from .kinds import request_kind_map as kindmap
|
||||
|
||||
@@ -70,7 +74,6 @@ class TOMLStrBuilder:
|
||||
|
||||
class TOMLDataExtractor:
|
||||
def __init__(self, file):
|
||||
self._data = dict()
|
||||
with open(file, "rb") as f:
|
||||
self._data = tomllib.load(f)
|
||||
|
||||
@@ -116,6 +119,8 @@ class Loader(metaclass=SingletonType):
|
||||
loads data into memory if not found
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("config.Loader")
|
||||
|
||||
def __init__(self, kind):
|
||||
self._kind = kind
|
||||
self._configs = dict()
|
||||
@@ -129,14 +134,16 @@ class Loader(metaclass=SingletonType):
|
||||
|
||||
def parse(self, identifier, data):
|
||||
if identifier in self._configs:
|
||||
print(f"config file with name {identifier} already in memory, skipping..")
|
||||
self.logger.info(
|
||||
f"config file with name {identifier} already in memory, skipping.."
|
||||
)
|
||||
return False
|
||||
self.parser = dataextraction_factory(data)
|
||||
return True
|
||||
|
||||
def register(self, identifier, data=None):
|
||||
self._configs[identifier] = data if data else self.parser.data
|
||||
print(f"config {self.name}/{identifier} loaded into memory")
|
||||
self.logger.info(f"config {self.name}/{identifier} loaded into memory")
|
||||
|
||||
def deregister(self):
|
||||
self._configs.clear()
|
||||
@@ -159,6 +166,7 @@ def loader(kind):
|
||||
|
||||
returns configs loaded into memory
|
||||
"""
|
||||
logger = logging.getLogger("config.loader")
|
||||
loader = Loader(kind)
|
||||
|
||||
for path in (
|
||||
@@ -167,7 +175,7 @@ def loader(kind):
|
||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||
):
|
||||
if path.is_dir():
|
||||
print(f"Checking [{path}] for TOML config files:")
|
||||
logger.info(f"Checking [{path}] for TOML config files:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
if loader.parse(identifier, file):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
class VMCMDErrors(Exception):
|
||||
class VBANCMDError(Exception):
|
||||
"""general errors"""
|
||||
|
||||
pass
|
||||
|
||||
55
vban_cmd/event.py
Normal file
55
vban_cmd/event.py
Normal file
@@ -0,0 +1,55 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
|
||||
|
||||
class Event:
|
||||
"""Keeps track of event subscriptions"""
|
||||
|
||||
logger = logging.getLogger("event.event")
|
||||
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
|
||||
def info(self, msg=None):
|
||||
info = (f"{msg} events",) if msg else ()
|
||||
if self.any():
|
||||
info += (f"now listening for {', '.join(self.get())} events",)
|
||||
else:
|
||||
info += (f"not listening for any events",)
|
||||
self.logger.info(", ".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@pdirty.setter
|
||||
def pdirty(self, val: bool):
|
||||
self.subs["pdirty"] = val
|
||||
self.info(f"pdirty {'added to' if val else 'removed from'}")
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
return self.subs["ldirty"]
|
||||
|
||||
@ldirty.setter
|
||||
def ldirty(self, val: bool):
|
||||
self.subs["ldirty"] = val
|
||||
self.info(f"ldirty {'added to' if val else 'removed from'}")
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, True)
|
||||
|
||||
def remove(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, False)
|
||||
@@ -1,15 +1,16 @@
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from functools import cached_property
|
||||
from typing import Iterable, NoReturn, Self
|
||||
from typing import Iterable, NoReturn
|
||||
|
||||
from .base import VbanCmd
|
||||
from .bus import request_bus_obj as bus
|
||||
from .command import Command
|
||||
from .config import request_config as configs
|
||||
from .kinds import KindMapClass
|
||||
from .kinds import request_kind_map as kindmap
|
||||
from .strip import request_strip_obj as strip
|
||||
from .vbancmd import VbanCmd
|
||||
|
||||
|
||||
class FactoryBuilder:
|
||||
@@ -19,6 +20,7 @@ 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):
|
||||
@@ -33,23 +35,23 @@ class FactoryBuilder:
|
||||
def _pinfo(self, name: str) -> NoReturn:
|
||||
"""prints progress status for each step"""
|
||||
name = name.split("_")[1]
|
||||
print(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
|
||||
def make_strip(self) -> Self:
|
||||
def make_strip(self):
|
||||
self._factory.strip = tuple(
|
||||
strip(i < self.kind.phys_in, self._factory, i)
|
||||
for i in range(self.kind.num_strip)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_bus(self) -> Self:
|
||||
def make_bus(self):
|
||||
self._factory.bus = tuple(
|
||||
bus(i < self.kind.phys_out, self._factory, i)
|
||||
for i in range(self.kind.num_bus)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_command(self) -> Self:
|
||||
def make_command(self):
|
||||
self._factory.command = Command.make(self._factory)
|
||||
return self
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@ class IRemote(metaclass=ABCMeta):
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd)
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
|
||||
def setter(self, param, val):
|
||||
"""Sends a string request RT packet."""
|
||||
@@ -120,8 +122,8 @@ class IRemote(metaclass=ABCMeta):
|
||||
if isinstance(val, bool):
|
||||
val = 1 if val else 0
|
||||
|
||||
self._remote.cache[f"{self.identifier}[{self.index}].{attr}"] = val
|
||||
script += f"{self.identifier}[{self.index}].{attr}={val};"
|
||||
self._remote.cache[f"{self.identifier}.{attr}"] = val
|
||||
script += f"{self.identifier}.{attr}={val};"
|
||||
|
||||
self._remote.sendtext(script)
|
||||
return self
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from functools import partial
|
||||
|
||||
from .error import VMCMDErrors
|
||||
from .util import cache_bool, cache_string
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
class Event:
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
|
||||
def info(self, msg):
|
||||
info = (
|
||||
f"{msg} events",
|
||||
f"Now listening for {', '.join(self.get())} events",
|
||||
)
|
||||
print("\n".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@property
|
||||
def ldirty(self):
|
||||
return self.subs["ldirty"]
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, event):
|
||||
self.subs[event] = True
|
||||
self.info(f"{event} added to")
|
||||
|
||||
def remove(self, event):
|
||||
self.subs[event] = False
|
||||
self.info(f"{event} removed from")
|
||||
@@ -1,6 +1,8 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator
|
||||
|
||||
from .util import comp
|
||||
|
||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||
VBAN_SERVICE_RTPACKET = 33
|
||||
MAX_PACKET_SIZE = 1436
|
||||
@@ -8,40 +10,27 @@ HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
||||
|
||||
|
||||
@dataclass
|
||||
class VBAN_VMRT_Packet_Data:
|
||||
"""Represents the structure of a VMRT data packet"""
|
||||
class VbanRtPacket:
|
||||
"""Represents the body of a VBAN RT data packet"""
|
||||
|
||||
_voicemeeterType: bytes
|
||||
_reserved: bytes
|
||||
_buffersize: bytes
|
||||
_voicemeeterVersion: bytes
|
||||
_optionBits: bytes
|
||||
_samplerate: bytes
|
||||
_inputLeveldB100: bytes
|
||||
_outputLeveldB100: bytes
|
||||
_TransportBit: bytes
|
||||
_stripState: bytes
|
||||
_busState: bytes
|
||||
_stripGaindB100Layer1: bytes
|
||||
_stripGaindB100Layer2: bytes
|
||||
_stripGaindB100Layer3: bytes
|
||||
_stripGaindB100Layer4: bytes
|
||||
_stripGaindB100Layer5: bytes
|
||||
_stripGaindB100Layer6: bytes
|
||||
_stripGaindB100Layer7: bytes
|
||||
_stripGaindB100Layer8: bytes
|
||||
_busGaindB100: bytes
|
||||
_stripLabelUTF8c60: bytes
|
||||
_busLabelUTF8c60: bytes
|
||||
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)
|
||||
|
||||
def pdirty(self, other):
|
||||
def _generate_levels(self, levelarray) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(levelarray[i : i + 2], "little")
|
||||
for i in range(0, len(levelarray), 2)
|
||||
)
|
||||
|
||||
def pdirty(self, other) -> bool:
|
||||
"""True iff any defined parameter has changed"""
|
||||
|
||||
return not (
|
||||
self._stripState == other._stripState
|
||||
and self._busState == other._busState
|
||||
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
|
||||
and self._busLabelUTF8c60 == other._busLabelUTF8c60
|
||||
and self._stripGaindB100Layer1 == other._stripGaindB100Layer1
|
||||
and self._stripGaindB100Layer2 == other._stripGaindB100Layer2
|
||||
and self._stripGaindB100Layer3 == other._stripGaindB100Layer3
|
||||
@@ -51,8 +40,17 @@ class VBAN_VMRT_Packet_Data:
|
||||
and self._stripGaindB100Layer7 == other._stripGaindB100Layer7
|
||||
and self._stripGaindB100Layer8 == other._stripGaindB100Layer8
|
||||
and self._busGaindB100 == other._busGaindB100
|
||||
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
|
||||
and self._busLabelUTF8c60 == other._busLabelUTF8c60
|
||||
)
|
||||
|
||||
def ldirty(self, strip_cache, bus_cache) -> bool:
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(not val for val in comp(strip_cache, self._strip_level)),
|
||||
tuple(not val for val in comp(bus_cache, self._bus_level)),
|
||||
)
|
||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
||||
|
||||
@property
|
||||
def voicemeetertype(self) -> str:
|
||||
"""returns voicemeeter type as a string"""
|
||||
@@ -77,24 +75,14 @@ class VBAN_VMRT_Packet_Data:
|
||||
return int.from_bytes(self._samplerate, "little")
|
||||
|
||||
@property
|
||||
def inputlevels(self) -> Generator[float, None, None]:
|
||||
"""returns the entire level array across all inputs"""
|
||||
for i in range(0, 68, 2):
|
||||
val = ((1 << 16) - 1) - int.from_bytes(
|
||||
self._inputLeveldB100[i : i + 2], "little"
|
||||
)
|
||||
if val != ((1 << 16) - 1):
|
||||
yield val
|
||||
def inputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all inputs for a kind"""
|
||||
return self._strip_level[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
|
||||
|
||||
@property
|
||||
def outputlevels(self) -> Generator[float, None, None]:
|
||||
"""returns the entire level array across all outputs"""
|
||||
for i in range(0, 128, 2):
|
||||
val = ((1 << 16) - 1) - int.from_bytes(
|
||||
self._outputLeveldB100[i : i + 2], "little"
|
||||
)
|
||||
if val != ((1 << 16) - 1):
|
||||
yield val
|
||||
def outputlevels(self) -> tuple:
|
||||
"""returns the entire level array across all outputs for a kind"""
|
||||
return self._bus_level[0 : 8 * self._kind.num_bus]
|
||||
|
||||
@property
|
||||
def stripstate(self) -> tuple:
|
||||
@@ -114,64 +102,56 @@ class VBAN_VMRT_Packet_Data:
|
||||
@property
|
||||
def stripgainlayer1(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer1[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer2(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer2[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer2[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer3(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer3[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer3[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer4(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer4[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer4[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer5(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer5[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer5[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer6(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer6[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer6[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer7(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer7[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer7[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer8(self) -> tuple:
|
||||
return tuple(
|
||||
((1 << 16) - 1)
|
||||
- int.from_bytes(self._stripGaindB100Layer8[i : i + 2], "little")
|
||||
int.from_bytes(self._stripGaindB100Layer8[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@@ -179,7 +159,7 @@ class VBAN_VMRT_Packet_Data:
|
||||
def busgain(self) -> tuple:
|
||||
"""returns tuple of bus gains"""
|
||||
return tuple(
|
||||
((1 << 16) - 1) - int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
||||
int.from_bytes(self._busGaindB100[i : i + 2], "little")
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@@ -201,8 +181,8 @@ class VBAN_VMRT_Packet_Data:
|
||||
|
||||
|
||||
@dataclass
|
||||
class VBAN_VMRT_Packet_Header:
|
||||
"""Represents a RESPONSE RT PACKET header"""
|
||||
class VbanRtPacketHeader:
|
||||
"""Represents the header of VBAN RT data packet"""
|
||||
|
||||
name = "Voicemeeter-RTP"
|
||||
vban: bytes = "VBAN".encode()
|
||||
@@ -225,7 +205,7 @@ class VBAN_VMRT_Packet_Header:
|
||||
|
||||
|
||||
@dataclass
|
||||
class TextRequestHeader:
|
||||
class RequestHeader:
|
||||
"""Represents a REQUEST RT PACKET header"""
|
||||
|
||||
name: str
|
||||
@@ -262,8 +242,8 @@ class TextRequestHeader:
|
||||
|
||||
|
||||
@dataclass
|
||||
class RegisterRTHeader:
|
||||
"""Represents a REGISTER RT PACKET header"""
|
||||
class SubscribeHeader:
|
||||
"""Represents a packet used to subscribe to the RT Packet Service"""
|
||||
|
||||
name = "Register RTP"
|
||||
timeout = 15
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from typing import Union
|
||||
|
||||
@@ -40,6 +41,14 @@ class Strip(IRemote):
|
||||
def gain(self, val: float):
|
||||
self.setter("gain", val)
|
||||
|
||||
def fadeto(self, target: float, time_: int):
|
||||
self.setter("FadeTo", f"({target}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
def fadeby(self, change: float, time_: int):
|
||||
self.setter("FadeBy", f"({change}, {time_})")
|
||||
time.sleep(self._remote.DELAY)
|
||||
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
def __str__(self):
|
||||
@@ -86,6 +95,12 @@ class VirtualStrip(Strip):
|
||||
def k(self, val: int):
|
||||
self.setter("karaoke", val)
|
||||
|
||||
def appgain(self, name: str, gain: float):
|
||||
self.setter("AppGain", f'("{name}", {gain})')
|
||||
|
||||
def appmute(self, name: str, mute: bool = None):
|
||||
self.setter("AppMute", f'("{name}", {1 if mute else 0})')
|
||||
|
||||
|
||||
class StripLevel(IRemote):
|
||||
def __init__(self, remote, index):
|
||||
@@ -103,9 +118,23 @@ class StripLevel(IRemote):
|
||||
self.range = self.level_map[self.index]
|
||||
|
||||
def getter(self):
|
||||
"""Returns a tuple of level values for the channel."""
|
||||
|
||||
def fget(i):
|
||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||
|
||||
if self._remote.running and self._remote.event.ldirty:
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote.cache["strip_level"][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
return tuple(
|
||||
round(-i * 0.01, 1)
|
||||
for i in self._remote.cache["strip_level"][self.range[0] : self.range[-1]]
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packet)[0][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -149,17 +178,12 @@ class GainLayer(IRemote):
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = getattr(self.public_packet, f"stripgainlayer{self._i+1}")[self.index]
|
||||
if val < 10000:
|
||||
return -val
|
||||
elif val == ((1 << 16) - 1):
|
||||
return 0
|
||||
else:
|
||||
return ((1 << 16) - 1) - val
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter(f"GainLayer[{self._i}]")
|
||||
if val is None:
|
||||
val = fget() * 0.01
|
||||
return round(val, 1)
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import logging
|
||||
|
||||
|
||||
class Subject:
|
||||
"""Adds support for observers"""
|
||||
|
||||
logger = logging.getLogger("subject.subject")
|
||||
|
||||
def __init__(self):
|
||||
"""list of current observers"""
|
||||
|
||||
@@ -22,16 +27,26 @@ class Subject:
|
||||
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
self.logger.info(f"{type(observer).__name__} added to event observers")
|
||||
else:
|
||||
print(f"Failed to add: {observer}")
|
||||
self.logger.error(
|
||||
f"Failed to add {type(observer).__name__} to event observers"
|
||||
)
|
||||
|
||||
register = add
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from _observers"""
|
||||
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
self.logger.info(f"{type(observer).__name__} removed from event observers")
|
||||
except ValueError:
|
||||
print(f"Failed to remove: {observer}")
|
||||
self.logger.error(
|
||||
f"Failed to remove {type(observer).__name__} from event observers"
|
||||
)
|
||||
|
||||
deregister = remove
|
||||
|
||||
def clear(self):
|
||||
"""clears the _observers list"""
|
||||
|
||||
@@ -10,6 +10,8 @@ def cache_bool(func, param):
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd) == 1
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -23,6 +25,8 @@ def cache_string(func, param):
|
||||
cmd = f"{self.identifier}.{param}"
|
||||
if cmd in self._remote.cache:
|
||||
return self._remote.cache.pop(cmd)
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
@@ -61,10 +65,12 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
||||
|
||||
Evaluates equality of each member in both tuples.
|
||||
"""
|
||||
|
||||
for a, b in zip(t0, t1):
|
||||
if b <= 9500:
|
||||
if ((1 << 16) - 1) - b <= 7200:
|
||||
yield a == b
|
||||
yield True
|
||||
else:
|
||||
yield True
|
||||
|
||||
|
||||
Socket = IntEnum("Socket", "register request response", start=0)
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
import logging
|
||||
import socket
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Iterable, NoReturn, Optional, Union
|
||||
from pathlib import Path
|
||||
from typing import Iterable, Optional, Union
|
||||
|
||||
from .misc import Event
|
||||
from .packet import TextRequestHeader
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
from .event import Event
|
||||
from .packet import RequestHeader
|
||||
from .subject import Subject
|
||||
from .util import Socket, comp, script
|
||||
from .util import Socket, script
|
||||
from .worker import Subscriber, Updater
|
||||
|
||||
|
||||
class VbanCmd(metaclass=ABCMeta):
|
||||
"""Base class responsible for communicating over VBAN RT Service"""
|
||||
"""Base class responsible for communicating with the VBAN RT Packet Service"""
|
||||
|
||||
DELAY = 0.001
|
||||
# fmt: off
|
||||
@@ -21,12 +28,17 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
1000000, 1500000, 2000000, 3000000,
|
||||
]
|
||||
# fmt: on
|
||||
logger = logging.getLogger("vbancmd.vbancmd")
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
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.text_header = TextRequestHeader(
|
||||
self.packet_request = RequestHeader(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
@@ -35,14 +47,22 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
||||
)
|
||||
self.subject = Subject()
|
||||
self.cache = dict()
|
||||
self.cache = {}
|
||||
self.event = Event(self.subs)
|
||||
self._pdirty = False
|
||||
self._ldirty = False
|
||||
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
|
||||
def _conn_from_toml(self) -> str:
|
||||
filepath = Path.cwd() / "vban.toml"
|
||||
with open(filepath, "rb") as f:
|
||||
conn = tomllib.load(f)
|
||||
return conn["connection"]
|
||||
|
||||
def __enter__(self):
|
||||
self.login()
|
||||
return self
|
||||
@@ -50,6 +70,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
def login(self):
|
||||
"""Starts the subscriber and updater threads"""
|
||||
self.running = True
|
||||
self.event.info()
|
||||
|
||||
self.subscriber = Subscriber(self)
|
||||
self.subscriber.start()
|
||||
@@ -57,6 +78,8 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
self.updater = Updater(self)
|
||||
self.updater.start()
|
||||
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
||||
|
||||
def _set_rt(
|
||||
self,
|
||||
id_: str,
|
||||
@@ -64,17 +87,15 @@ 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 = id_ if not param else f"{id_}.{param}={val};"
|
||||
self.socks[Socket.request].sendto(
|
||||
self.text_header.header + cmd.encode(),
|
||||
self.packet_request.header + cmd.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
count = int.from_bytes(self.text_header.framecounter, "little") + 1
|
||||
self.text_header.framecounter = count.to_bytes(4, "little")
|
||||
count = int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||
self.packet_request.framecounter = count.to_bytes(4, "little")
|
||||
if param:
|
||||
self.cache[f"{id_}.{param}"] = val
|
||||
if self.sync:
|
||||
time.sleep(0.02)
|
||||
|
||||
@script
|
||||
def sendtext(self, cmd):
|
||||
@@ -90,8 +111,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
v1, v2, v3, v4 = self.public_packet.voicemeeterversion
|
||||
return f"{v1}.{v2}.{v3}.{v4}"
|
||||
return "{0}.{1}.{2}.{3}".format(*self.public_packet.voicemeeterversion)
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
@@ -101,11 +121,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
@property
|
||||
def ldirty(self):
|
||||
"""True iff a level value has changed."""
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(not x for x in comp(self.cache["strip_level"], self._strip_buf)),
|
||||
tuple(not x for x in comp(self.cache["bus_level"], self._bus_buf)),
|
||||
)
|
||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
||||
return self._ldirty
|
||||
|
||||
@property
|
||||
def public_packet(self):
|
||||
@@ -113,7 +129,7 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
|
||||
def clear_dirty(self):
|
||||
while self.pdirty:
|
||||
pass
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
def _get_levels(self, packet) -> Iterable:
|
||||
"""
|
||||
@@ -122,8 +138,8 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
strip levels in PREFADER mode.
|
||||
"""
|
||||
return (
|
||||
tuple(val for val in packet.inputlevels),
|
||||
tuple(val for val in packet.outputlevels),
|
||||
packet.inputlevels,
|
||||
packet.outputlevels,
|
||||
)
|
||||
|
||||
def apply(self, data: dict):
|
||||
@@ -151,14 +167,15 @@ class VbanCmd(metaclass=ABCMeta):
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
print(f"Profile '{name}' applied!")
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
except KeyError as e:
|
||||
print(("\n").join(error_msg))
|
||||
self.logger.error(("\n").join(error_msg))
|
||||
|
||||
def logout(self):
|
||||
self.running = False
|
||||
time.sleep(0.2)
|
||||
[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):
|
||||
self.logout()
|
||||
@@ -1,40 +1,36 @@
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from typing import Optional
|
||||
|
||||
from .packet import (
|
||||
HEADER_SIZE,
|
||||
RegisterRTHeader,
|
||||
VBAN_VMRT_Packet_Data,
|
||||
VBAN_VMRT_Packet_Header,
|
||||
)
|
||||
from .util import Socket
|
||||
from .error import VBANCMDError
|
||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
||||
from .util import Socket, comp
|
||||
|
||||
|
||||
class Subscriber(threading.Thread):
|
||||
"""fire a subscription packet every 10 seconds"""
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="subscriber", target=self.register, daemon=True)
|
||||
self._rem = remote
|
||||
self.register_header = RegisterRTHeader()
|
||||
super().__init__(name="subscriber", target=self.subscribe, daemon=True)
|
||||
self._remote = remote
|
||||
self.packet = SubscribeHeader()
|
||||
|
||||
def register(self):
|
||||
while self._rem.running:
|
||||
def subscribe(self):
|
||||
while self._remote.running:
|
||||
try:
|
||||
self._rem.socks[Socket.register].sendto(
|
||||
self.register_header.header,
|
||||
(socket.gethostbyname(self._rem.ip), self._rem.port),
|
||||
self._remote.socks[Socket.register].sendto(
|
||||
self.packet.header,
|
||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
||||
)
|
||||
count = int.from_bytes(self.register_header.framecounter, "little") + 1
|
||||
self.register_header.framecounter = count.to_bytes(4, "little")
|
||||
count = int.from_bytes(self.packet.framecounter, "little") + 1
|
||||
self.packet.framecounter = count.to_bytes(4, "little")
|
||||
time.sleep(10)
|
||||
except socket.gaierror as e:
|
||||
print(f"Unable to resolve hostname {self._rem.ip}")
|
||||
self._rem.socks[Socket.register].close()
|
||||
raise e
|
||||
except socket.gaierror:
|
||||
err_msg = f"Unable to resolve hostname {self._remote.ip}"
|
||||
print(err_msg)
|
||||
raise VBANCMDError(err_msg)
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
@@ -44,80 +40,103 @@ class Updater(threading.Thread):
|
||||
notifies observers of event updates
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("worker.updater")
|
||||
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="updater", target=self.update, daemon=True)
|
||||
self._rem = remote
|
||||
self._rem.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._rem.port)
|
||||
self._remote = remote
|
||||
self._remote.socks[Socket.response].settimeout(5)
|
||||
self._remote.socks[Socket.response].bind(
|
||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||
)
|
||||
self.expected_packet = VBAN_VMRT_Packet_Header()
|
||||
self._rem._public_packet = self._get_rt()
|
||||
self.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 _fetch_rt_packet(self) -> Optional[VBAN_VMRT_Packet_Data]:
|
||||
"""Returns a valid RT Data Packet or None"""
|
||||
data, _ = self._rem.socks[Socket.response].recvfrom(2048)
|
||||
# check for packet data
|
||||
if len(data) > HEADER_SIZE:
|
||||
# check if packet is of type VBAN
|
||||
if self.expected_packet.header == data[: HEADER_SIZE - 4]:
|
||||
return VBAN_VMRT_Packet_Data(
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
_inputLeveldB100=data[44:112],
|
||||
_outputLeveldB100=data[112:240],
|
||||
_TransportBit=data[240:244],
|
||||
_stripState=data[244:276],
|
||||
_busState=data[276:308],
|
||||
_stripGaindB100Layer1=data[308:324],
|
||||
_stripGaindB100Layer2=data[324:340],
|
||||
_stripGaindB100Layer3=data[340:356],
|
||||
_stripGaindB100Layer4=data[356:372],
|
||||
_stripGaindB100Layer5=data[372:388],
|
||||
_stripGaindB100Layer6=data[388:404],
|
||||
_stripGaindB100Layer7=data[404:420],
|
||||
_stripGaindB100Layer8=data[420:436],
|
||||
_busGaindB100=data[436:452],
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||
try:
|
||||
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
||||
# check for packet data
|
||||
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],
|
||||
_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:
|
||||
err_msg = f"Unable to establish connection with {self._remote.ip}"
|
||||
print(err_msg)
|
||||
raise VBANCMDError(err_msg)
|
||||
|
||||
def _get_rt(self) -> VBAN_VMRT_Packet_Data:
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
def fget():
|
||||
data = False
|
||||
data = None
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
time.sleep(self._rem.DELAY)
|
||||
time.sleep(self._remote.DELAY)
|
||||
return data
|
||||
|
||||
return fget()
|
||||
|
||||
def update(self):
|
||||
print(f"Listening for {', '.join(self._rem.event.get())} events")
|
||||
(
|
||||
self._rem.cache["strip_level"],
|
||||
self._rem.cache["bus_level"],
|
||||
) = self._rem._get_levels(self._rem.public_packet)
|
||||
|
||||
while self._rem.running:
|
||||
while self._remote.running:
|
||||
start = time.time()
|
||||
_pp = self._get_rt()
|
||||
self._rem._strip_buf, self._rem._bus_buf = self._rem._get_levels(_pp)
|
||||
self._rem._pdirty = _pp.pdirty(self._rem.public_packet)
|
||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||
ldirty = _pp.ldirty(
|
||||
self._remote.cache["strip_level"], self._remote.cache["bus_level"]
|
||||
)
|
||||
|
||||
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._rem.event.ldirty and self._rem.ldirty:
|
||||
self._rem.cache["strip_level"] = self._rem._strip_buf
|
||||
self._rem.cache["bus_level"] = self._rem._bus_buf
|
||||
self._rem.subject.notify("ldirty")
|
||||
if self._rem.public_packet != _pp:
|
||||
self._rem._public_packet = _pp
|
||||
if self._rem.event.pdirty and self._rem.pdirty:
|
||||
self._rem.subject.notify("pdirty")
|
||||
elapsed = time.time() - start
|
||||
if self._rem.ratelimit - elapsed > 0:
|
||||
time.sleep(self._rem.ratelimit - elapsed)
|
||||
if self._remote.ratelimit - elapsed > 0:
|
||||
time.sleep(self._remote.ratelimit - elapsed)
|
||||
|
||||
Reference in New Issue
Block a user