48 Commits

Author SHA1 Message Date
onyx-and-iris
f6218d2032 add scripts.py 2022-11-07 20:26:06 +00:00
onyx-and-iris
4aacc60857 md fix 2022-11-05 12:16:25 +00:00
norm
8f9ac47d02 fix apply in readme. 2022-11-05 03:14:37 +00:00
norm
90e994c193 typo fix 2022-11-05 02:48:33 +00:00
onyx-and-iris
44cd13aa48 refactor examples
add scripts to pyproject
2022-10-28 20:19:05 +01:00
onyx-and-iris
87eb61170e blacken readme example.
fix bug in main.py
2022-10-19 21:10:59 +01:00
onyx-and-iris
01c99d5b31 init ldirty
patch bump
2022-10-19 14:32:54 +01:00
onyx-and-iris
3144a95e07 minor bump 2022-10-19 14:21:23 +01:00
onyx-and-iris
1833b28c8d Connection section added to README.
CHANGELOG updated to reflect changes.
2022-10-19 14:21:04 +01:00
onyx-and-iris
ee3a871d23 add a delimiter end of request string in _set_rt
fixes bug if more than a single command in request packet.

removed [{self.index}] from apply string. (duplicates)
2022-10-19 14:20:23 +01:00
onyx-and-iris
197f81aa73 assume vban.toml for observer example
add README to observer example
2022-10-18 15:20:20 +01:00
onyx-and-iris
362873c5be fix vban config name in example readme 2022-10-17 13:15:02 +01:00
onyx-and-iris
c86f7971b0 rewording in obs example 2022-10-17 13:14:08 +01:00
onyx-and-iris
bac60e5ed3 add vban.toml to gitignore
minor bump
2022-10-07 20:01:55 +01:00
onyx-and-iris
692acc8dd0 assume vban.toml in obs example
update README for obs example
2022-10-07 20:01:29 +01:00
onyx-and-iris
d57269f147 add ability to read conn info from toml 2022-10-07 20:00:56 +01:00
onyx-and-iris
be69d905c4 minor ver bump 2022-10-06 20:30:14 +01:00
onyx-and-iris
5ceb8f775a config.toml added to gitignore 2022-10-06 20:29:38 +01:00
onyx-and-iris
e0f4aab257 obs example added.
README for obs example added
2022-10-06 20:29:03 +01:00
onyx-and-iris
4ee37f54c5 fadto() fadeby() methods added to strip/bus classes
appgain(), appmute() methods added to virtualstrip class
2022-10-06 20:28:26 +01:00
onyx-and-iris
550df917fb add, remove now accept iterables
update README

patch bump
2022-10-06 18:07:41 +01:00
onyx-and-iris
2f82e0b1fc fix str format 2022-10-06 16:50:03 +01:00
onyx-and-iris
0c60fe3d5e add property setters in event class
use event property setters in examples

update README

patch bump
2022-10-06 16:45:15 +01:00
onyx-and-iris
243a43ac22 patch bump 2022-10-05 22:54:39 +01:00
onyx-and-iris
49354d6d55 lower threshold a level is considered dirty 2022-10-05 22:54:26 +01:00
onyx-and-iris
5c9ac4d78f patch bump 2022-10-04 15:43:56 +01:00
onyx-and-iris
02b21b6989 print bus level values in observer example 2022-10-04 15:43:09 +01:00
onyx-and-iris
4659cf7cdb util:
in comp, consider level value clean if below -60.0

vbancmd:
pass tuple expansion into string format in version method.
ldirty and _get_levels logic now moved into rt packet class
2022-10-04 15:42:36 +01:00
onyx-and-iris
8663aab2ce add fget() to level getters in strip, bus 2022-10-04 15:40:32 +01:00
onyx-and-iris
a029011012 vbanrtpacket refactored
_generate_levels method added
ldirty method added.

moved initialize strip_level, bus_level cache into updater init()
initialize comps in updater init()
2022-10-04 15:39:56 +01:00
onyx-and-iris
bfa1a718f9 user logger in apply_config
patch bump
2022-09-29 12:34:02 +01:00
onyx-and-iris
2048a807d1 move event info logging from Updater into VbanCmd
odd logout logging

patch bump
2022-09-29 11:48:30 +01:00
onyx-and-iris
566bff3ced move vbancmd class section in readme 2022-09-28 20:01:17 +01:00
onyx-and-iris
70dbee6f02 update changelog to refect changes 2022-09-28 18:31:35 +01:00
onyx-and-iris
c14196fc31 minor version bump 2022-09-28 18:20:25 +01:00
onyx-and-iris
c28398c5f6 vban.subject subsection added to README under Events 2022-09-28 18:15:08 +01:00
onyx-and-iris
5177c2d297 fix erroneous call to self.vm
logging level INFO added
2022-09-28 18:14:06 +01:00
onyx-and-iris
23bc15e437 logging module now used to log interface events.
register, deregister method aliases added to Subject class.
2022-09-28 18:13:07 +01:00
onyx-and-iris
db96872965 changes to level/gain properties in VbanRtPacket
level getters in strip, bus fetch from public packet if not in cache
2022-09-28 18:07:10 +01:00
onyx-and-iris
1169435104 base renamed to vbancmd
misc renamed to event

info message fixed if no events subbed to

now using logging module in Event class
2022-09-28 18:03:22 +01:00
onyx-and-iris
f46abedf12 fix name of base error class in readme
patch bump
2022-09-24 07:49:17 +01:00
onyx-and-iris
733fab45b4 raise VBANCMD error on connection failure.
leave teardown procedures to consumer library. (or context manager)
2022-09-24 07:45:28 +01:00
onyx-and-iris
444f95a9d6 add timeout to response socket in updater
patch bump
2022-09-23 20:03:16 +01:00
onyx-and-iris
14e538dca6 patch bump 2022-09-03 20:43:47 +01:00
onyx-and-iris
af5e81c339 remove debug print 2022-09-03 20:41:26 +01:00
onyx-and-iris
aadfbd3925 fix regression causing pdirty update to fail.
patch bump
2022-09-03 20:35:37 +01:00
onyx-and-iris
4ef3d1f225 tomli/tomllib compatibility layer added.
Type annotation Self removed.

python version requirement changed.

tomli added as runtime dependency if py ver < 3.11

minor version bump.
2022-09-03 16:47:38 +01:00
onyx-and-iris
aea2be624e clean up class names in packet module.
add __init__ to vbanrtpacket class.

patch bump
2022-08-10 17:49:21 +01:00
26 changed files with 747 additions and 436 deletions

10
.gitignore vendored
View File

@@ -1,6 +1,3 @@
# quick test
quick.py
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.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 # 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. # option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/ #.idea/
# quick test
quick.py
#config
config.toml
vban.toml

View File

@@ -11,6 +11,68 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x] - [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 ## [1.3.0] - 2022-08-02
### Added ### Added

172
README.md
View File

@@ -25,24 +25,35 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
## Requirements ## Requirements
- [Voicemeeter](https://voicemeeter.com/) - [Voicemeeter](https://voicemeeter.com/)
- Python 3.11 or greater - Python 3.10 or greater
## Installation ## Installation
### `Pip`
Install vban-cmd package from your console
`pip install vban-cmd` `pip install vban-cmd`
## `Use` ## `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. 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. Login and logout are handled for you in this scenario.
#### `__main__.py`
```python ```python
import vban_cmd import vban_cmd
@@ -59,17 +70,21 @@ class ManyThings:
) )
def other_things(self): def other_things(self):
self.vban.bus[3].gain = -6.3
self.vban.bus[4].eq = True
info = ( info = (
f"bus 3 gain has been set to {self.vban.bus[3].gain}", f"bus 3 gain has been set to {self.vban.bus[3].gain}",
f"bus 4 eq has been set to {self.vban.bus[4].eq}", f"bus 4 eq has been set to {self.vban.bus[4].eq}",
) )
self.vban.bus[3].gain = -6.3
self.vban.bus[4].eq = True
print("\n".join(info)) print("\n".join(info))
def main(): 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 = ManyThings(vban)
do.things() do.things()
do.other_things() do.other_things()
@@ -84,13 +99,6 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
kind_id = "banana"
opts = {
"ip": "<ip address>",
"streamname": "Command1",
"port": 6980,
}
main() main()
``` ```
@@ -124,7 +132,24 @@ example:
```python ```python
vban.strip[3].gain = 3.7 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 ##### Gainlayers
@@ -213,6 +238,22 @@ print(vban.bus[0].levels.all)
`levels.all` will return -200.0 if no audio detected. `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 ### Command
Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available: Certain 'special' commands are defined by the API as performing actions rather than setting values. The following methods are available:
@@ -242,8 +283,8 @@ vban.command.showvbanchat = true
```python ```python
vban.apply( vban.apply(
{ {
"strip-2": {"A1": True, "B1": True, "gain": -6.0}, "strip-0": {"A1": True, "B1": True, "gain": -6.0},
"bus-2": {"mute": True}, "bus-1": {"mute": True, "mode": "composite"},
} }
) )
``` ```
@@ -252,7 +293,7 @@ Or for each class you may do:
```python ```python
vban.strip[0].apply(mute: true, gain: 3.2, A1: true) vban.strip[0].apply(mute: true, gain: 3.2, A1: true)
vban.vban.outstream[0].apply(on: true, name: 'streamname', bit: 24) vban.bus[0].apply(A1: true)
``` ```
## Config Files ## 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. will load a config file at configs/banana/example.toml for Voicemeeter Banana.
## `Base Module` ## Events
### VbanCmd class Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
`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.
example: 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: example:
```python ```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 # get a list of currently subscribed
print(vban.event.get()) 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` #### `vban.pdirty`
True iff a parameter has been changed. 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`
- `errors.VMCMDErrors`: Base VMCMD error class. - `errors.VBANCMDError`: Base VMCMD error class.
### `Tests` ### `Tests`

View File

@@ -13,17 +13,21 @@ class ManyThings:
) )
def other_things(self): def other_things(self):
self.vban.bus[3].gain = -6.3
self.vban.bus[4].eq = True
info = ( info = (
f"bus 3 gain has been set to {self.vban.bus[3].gain}", f"bus 3 gain has been set to {self.vban.bus[3].gain}",
f"bus 4 eq has been set to {self.vban.bus[4].eq}", f"bus 4 eq has been set to {self.vban.bus[4].eq}",
) )
self.vban.bus[3].gain = -6.3
self.vban.bus[4].eq = True
print("\n".join(info)) print("\n".join(info))
def main(): 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 = ManyThings(vban)
do.things() do.things()
do.other_things() do.other_things()
@@ -33,19 +37,9 @@ def main():
{ {
"strip-2": {"A1": True, "B1": True, "gain": -6.0}, "strip-2": {"A1": True, "B1": True, "gain": -6.0},
"bus-2": {"mute": True}, "bus-2": {"mute": True},
"button-0": {"state": True},
"vban-in-0": {"on": True},
"vban-out-1": {"name": "streamname"},
} }
) )
if __name__ == "__main__": if __name__ == "__main__":
kind_id = "banana"
opts = {
"ip": "<ip address>",
"streamname": "Command1",
"port": 6980,
}
main() main()

51
examples/obs/README.md Normal file
View 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
View 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()

View 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.

View File

@@ -1,35 +1,33 @@
import logging
import vban_cmd import vban_cmd
logging.basicConfig(level=logging.INFO)
class Observer: class Observer:
def __init__(self, vban): def __init__(self, vban):
self.vban = vban self.vban = vban
# register your app as event observer # register your app as event observer
self.vban.subject.add(self) self.vban.subject.add(self)
# add level updates, since they are disabled by default. # enable level updates, since they are disabled by default.
self.vm.event.add("ldirty") self.vban.event.ldirty = True
# define an 'on_update' callback function to receive event updates # define an 'on_update' callback function to receive event updates
def on_update(self, subject): def on_update(self, subject):
if subject == "pdirty": if subject == "pdirty":
print("pdirty!") print("pdirty!")
elif subject == "ldirty": elif subject == "ldirty":
info = ( for bus in self.vban.bus:
f"[{self.vban.bus[0]} {self.vban.bus[0].levels.isdirty}]", if bus.levels.isdirty:
f"[{self.vban.bus[1]} {self.vban.bus[1].levels.isdirty}]", print(bus, bus.levels.all)
f"[{self.vban.bus[2]} {self.vban.bus[2].levels.isdirty}]",
f"[{self.vban.bus[3]} {self.vban.bus[3].levels.isdirty}]",
f"[{self.vban.bus[4]} {self.vban.bus[4].levels.isdirty}]",
f"[{self.vban.bus[5]} {self.vban.bus[5].levels.isdirty}]",
f"[{self.vban.bus[6]} {self.vban.bus[6].levels.isdirty}]",
f"[{self.vban.bus[7]} {self.vban.bus[7].levels.isdirty}]",
)
print(" ".join(info))
def main(): def main():
with vban_cmd.api(kind_id, **opts) as vban: kind_id = "potato"
obs = Observer(vban)
with vban_cmd.api(kind_id) as vban:
Observer(vban)
while cmd := input("Press <Enter> to exit\n"): while cmd := input("Press <Enter> to exit\n"):
if not cmd: if not cmd:
@@ -37,11 +35,4 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
kind_id = "potato"
opts = {
"ip": "<ip address>",
"streamname": "Command1",
"port": 6980,
}
main() main()

125
poetry.lock generated
View File

@@ -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]] [[package]]
name = "attrs" name = "attrs"
version = "22.1.0" version = "22.1.0"
@@ -22,7 +14,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
[[package]] [[package]]
name = "black" name = "black"
version = "22.6.0" version = "22.8.0"
description = "The uncompromising code formatter." description = "The uncompromising code formatter."
category = "dev" category = "dev"
optional = false optional = false
@@ -33,6 +25,7 @@ click = ">=8.0.0"
mypy-extensions = ">=0.4.3" mypy-extensions = ">=0.4.3"
pathspec = ">=0.9.0" pathspec = ">=0.9.0"
platformdirs = ">=2" platformdirs = ">=2"
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
[package.extras] [package.extras]
colorama = ["colorama (>=0.4.3)"] colorama = ["colorama (>=0.4.3)"]
@@ -102,11 +95,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
[[package]] [[package]]
name = "pathspec" name = "pathspec"
version = "0.9.0" version = "0.10.1"
description = "Utility library for gitignore style pattern matching of file paths." description = "Utility library for gitignore style pattern matching of file paths."
category = "dev" category = "dev"
optional = false optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" python-versions = ">=3.7"
[[package]] [[package]]
name = "platformdirs" name = "platformdirs"
@@ -129,8 +122,8 @@ optional = false
python-versions = ">=3.6" python-versions = ">=3.6"
[package.extras] [package.extras]
dev = ["pre-commit", "tox"] testing = ["pytest-benchmark", "pytest"]
testing = ["pytest", "pytest-benchmark"] dev = ["tox", "pre-commit"]
[[package]] [[package]]
name = "py" name = "py"
@@ -153,14 +146,13 @@ diagrams = ["railroad-diagrams", "jinja2"]
[[package]] [[package]]
name = "pytest" name = "pytest"
version = "7.1.2" version = "7.1.3"
description = "pytest: simple powerful testing with Python" description = "pytest: simple powerful testing with Python"
category = "dev" category = "dev"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[package.dependencies] [package.dependencies]
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
attrs = ">=19.2.0" attrs = ">=19.2.0"
colorama = {version = "*", markers = "sys_platform == \"win32\""} colorama = {version = "*", markers = "sys_platform == \"win32\""}
iniconfig = "*" iniconfig = "*"
@@ -198,97 +190,30 @@ pytest = ">=3.6"
name = "tomli" name = "tomli"
version = "2.0.1" version = "2.0.1"
description = "A lil' TOML parser" description = "A lil' TOML parser"
category = "dev" category = "main"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
[metadata] [metadata]
lock-version = "1.1" lock-version = "1.1"
python-versions = "^3.11" python-versions = "^3.10"
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014" content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
[metadata.files] [metadata.files]
atomicwrites = []
attrs = [] attrs = []
black = [ black = []
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, click = []
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, colorama = []
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, iniconfig = []
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, isort = []
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, mypy-extensions = []
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, packaging = []
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, pathspec = []
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, platformdirs = []
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, pluggy = []
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, py = []
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, pyparsing = []
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, pytest = []
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, pytest-randomly = []
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
]
click = [
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
]
colorama = [
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
]
iniconfig = [
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
]
isort = [
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
]
mypy-extensions = [
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
]
packaging = [
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
]
pathspec = [
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
]
platformdirs = [
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
]
pluggy = [
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
]
py = [
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
]
pyparsing = [
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
]
pytest = [
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
]
pytest-randomly = [
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
]
pytest-repeat = [] pytest-repeat = []
tomli = [ tomli = []
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
]

View File

@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "vban-cmd" name = "vban-cmd"
version = "1.3.2" version = "1.8.1"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
authors = ["onyx-and-iris <code@onyxandiris.online>"] authors = ["onyx-and-iris <code@onyxandiris.online>"]
license = "MIT" license = "MIT"
@@ -8,7 +8,8 @@ readme = "README.md"
repository = "https://github.com/onyx-and-iris/vban-cmd-python" repository = "https://github.com/onyx-and-iris/vban-cmd-python"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.10"
tomli = { version = "^2.0.1", python = "<3.11" }
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]
@@ -21,3 +22,7 @@ isort = "^5.10.1"
[build-system] [build-system]
requires = ["poetry-core>=1.0.0"] requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api" build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
obs = "scripts:ex_obs"
observer = "scripts:ex_observer"

12
scripts.py Normal file
View 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)])

View File

@@ -32,22 +32,25 @@ class Bus(IRemote):
def gain(self) -> float: def gain(self) -> float:
def fget(): def fget():
val = self.public_packet.busgain[self.index] val = self.public_packet.busgain[self.index]
if val < 10000: if 0 <= val <= 1200:
return -val return val * 0.01
elif val == ((1 << 16) - 1): return (((1 << 16) - 1) - val) * -0.01
return 0
else:
return ((1 << 16) - 1) - val
val = self.getter("gain") val = self.getter("gain")
if val is None: return round(val if val else fget(), 1)
val = fget() * 0.01
return round(val, 1)
@gain.setter @gain.setter
def gain(self, val: float): def gain(self, val: float):
self.setter("gain", val) self.setter("gain", val)
def fadeto(self, target: float, time_: int):
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): class PhysicalBus(Bus):
def __str__(self): def __str__(self):
@@ -79,10 +82,20 @@ class BusLevel(IRemote):
def getter(self): def getter(self):
"""Returns a tuple of level values for the channel.""" """Returns a tuple of level values for the channel."""
def fget(i):
return round((((1 << 16) - 1) - i) * -0.01, 1)
if self._remote.running and self._remote.event.ldirty:
return tuple( return tuple(
round(-i * 0.01, 1) fget(i)
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]] for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
) )
return tuple(
fget(i)
for i in self._remote._get_levels(self.public_packet)[1][
self.range[0] : self.range[-1]
]
)
@property @property
def identifier(self) -> str: def identifier(self) -> str:

View File

@@ -1,4 +1,3 @@
from .error import VMCMDErrors
from .iremote import IRemote from .iremote import IRemote
from .meta import action_prop from .meta import action_prop

View File

@@ -1,7 +1,11 @@
import itertools import itertools
import logging
from pathlib import Path from pathlib import Path
try:
import tomllib import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from .kinds import request_kind_map as kindmap from .kinds import request_kind_map as kindmap
@@ -70,7 +74,6 @@ class TOMLStrBuilder:
class TOMLDataExtractor: class TOMLDataExtractor:
def __init__(self, file): def __init__(self, file):
self._data = dict()
with open(file, "rb") as f: with open(file, "rb") as f:
self._data = tomllib.load(f) self._data = tomllib.load(f)
@@ -116,6 +119,8 @@ class Loader(metaclass=SingletonType):
loads data into memory if not found loads data into memory if not found
""" """
logger = logging.getLogger("config.Loader")
def __init__(self, kind): def __init__(self, kind):
self._kind = kind self._kind = kind
self._configs = dict() self._configs = dict()
@@ -129,14 +134,16 @@ class Loader(metaclass=SingletonType):
def parse(self, identifier, data): def parse(self, identifier, data):
if identifier in self._configs: 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 return False
self.parser = dataextraction_factory(data) self.parser = dataextraction_factory(data)
return True return True
def register(self, identifier, data=None): def register(self, identifier, data=None):
self._configs[identifier] = data if data else self.parser.data self._configs[identifier] = data if data else self.parser.data
print(f"config {self.name}/{identifier} loaded into memory") self.logger.info(f"config {self.name}/{identifier} loaded into memory")
def deregister(self): def deregister(self):
self._configs.clear() self._configs.clear()
@@ -159,6 +166,7 @@ def loader(kind):
returns configs loaded into memory returns configs loaded into memory
""" """
logger = logging.getLogger("config.loader")
loader = Loader(kind) loader = Loader(kind)
for path in ( for path in (
@@ -167,7 +175,7 @@ def loader(kind):
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name, Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
): ):
if path.is_dir(): 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"): for file in path.glob("*.toml"):
identifier = file.with_suffix("").stem identifier = file.with_suffix("").stem
if loader.parse(identifier, file): if loader.parse(identifier, file):

View File

@@ -1,4 +1,4 @@
class VMCMDErrors(Exception): class VBANCMDError(Exception):
"""general errors""" """general errors"""
pass pass

55
vban_cmd/event.py Normal file
View 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)

View File

@@ -1,15 +1,16 @@
import logging
from abc import abstractmethod from abc import abstractmethod
from enum import IntEnum from enum import IntEnum
from functools import cached_property from functools import cached_property
from typing import Iterable, NoReturn, Self from typing import Iterable, NoReturn
from .base import VbanCmd
from .bus import request_bus_obj as bus from .bus import request_bus_obj as bus
from .command import Command from .command import Command
from .config import request_config as configs from .config import request_config as configs
from .kinds import KindMapClass from .kinds import KindMapClass
from .kinds import request_kind_map as kindmap from .kinds import request_kind_map as kindmap
from .strip import request_strip_obj as strip from .strip import request_strip_obj as strip
from .vbancmd import VbanCmd
class FactoryBuilder: class FactoryBuilder:
@@ -19,6 +20,7 @@ class FactoryBuilder:
Separates construction from representation. Separates construction from representation.
""" """
logger = logging.getLogger("vbancmd.factorybuilder")
BuilderProgress = IntEnum("BuilderProgress", "strip bus command", start=0) BuilderProgress = IntEnum("BuilderProgress", "strip bus command", start=0)
def __init__(self, factory, kind: KindMapClass): def __init__(self, factory, kind: KindMapClass):
@@ -33,23 +35,23 @@ class FactoryBuilder:
def _pinfo(self, name: str) -> NoReturn: def _pinfo(self, name: str) -> NoReturn:
"""prints progress status for each step""" """prints progress status for each step"""
name = name.split("_")[1] 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( self._factory.strip = tuple(
strip(i < self.kind.phys_in, self._factory, i) strip(i < self.kind.phys_in, self._factory, i)
for i in range(self.kind.num_strip) for i in range(self.kind.num_strip)
) )
return self return self
def make_bus(self) -> Self: def make_bus(self):
self._factory.bus = tuple( self._factory.bus = tuple(
bus(i < self.kind.phys_out, self._factory, i) bus(i < self.kind.phys_out, self._factory, i)
for i in range(self.kind.num_bus) for i in range(self.kind.num_bus)
) )
return self return self
def make_command(self) -> Self: def make_command(self):
self._factory.command = Command.make(self._factory) self._factory.command = Command.make(self._factory)
return self return self

View File

@@ -91,6 +91,8 @@ class IRemote(metaclass=ABCMeta):
cmd = f"{self.identifier}.{param}" cmd = f"{self.identifier}.{param}"
if cmd in self._remote.cache: if cmd in self._remote.cache:
return self._remote.cache.pop(cmd) return self._remote.cache.pop(cmd)
if self._remote.sync:
self._remote.clear_dirty()
def setter(self, param, val): def setter(self, param, val):
"""Sends a string request RT packet.""" """Sends a string request RT packet."""
@@ -120,8 +122,8 @@ class IRemote(metaclass=ABCMeta):
if isinstance(val, bool): if isinstance(val, bool):
val = 1 if val else 0 val = 1 if val else 0
self._remote.cache[f"{self.identifier}[{self.index}].{attr}"] = val self._remote.cache[f"{self.identifier}.{attr}"] = val
script += f"{self.identifier}[{self.index}].{attr}={val};" script += f"{self.identifier}.{attr}={val};"
self._remote.sendtext(script) self._remote.sendtext(script)
return self return self

View File

@@ -1,6 +1,5 @@
from functools import partial from functools import partial
from .error import VMCMDErrors
from .util import cache_bool, cache_string from .util import cache_bool, cache_string

View File

@@ -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")

View File

@@ -1,6 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Generator from typing import Generator
from .util import comp
VBAN_SERVICE_RTPACKETREGISTER = 32 VBAN_SERVICE_RTPACKETREGISTER = 32
VBAN_SERVICE_RTPACKET = 33 VBAN_SERVICE_RTPACKET = 33
MAX_PACKET_SIZE = 1436 MAX_PACKET_SIZE = 1436
@@ -8,40 +10,27 @@ HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
@dataclass @dataclass
class VBAN_VMRT_Packet_Data: class VbanRtPacket:
"""Represents the structure of a VMRT data packet""" """Represents the body of a VBAN RT data packet"""
_voicemeeterType: bytes def __init__(self, **kwargs):
_reserved: bytes for k, v in kwargs.items():
_buffersize: bytes setattr(self, k, v)
_voicemeeterVersion: bytes self._strip_level = self._generate_levels(self._inputLeveldB100)
_optionBits: bytes self._bus_level = self._generate_levels(self._outputLeveldB100)
_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 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""" """True iff any defined parameter has changed"""
return not ( return not (
self._stripState == other._stripState self._stripState == other._stripState
and self._busState == other._busState and self._busState == other._busState
and self._stripLabelUTF8c60 == other._stripLabelUTF8c60
and self._busLabelUTF8c60 == other._busLabelUTF8c60
and self._stripGaindB100Layer1 == other._stripGaindB100Layer1 and self._stripGaindB100Layer1 == other._stripGaindB100Layer1
and self._stripGaindB100Layer2 == other._stripGaindB100Layer2 and self._stripGaindB100Layer2 == other._stripGaindB100Layer2
and self._stripGaindB100Layer3 == other._stripGaindB100Layer3 and self._stripGaindB100Layer3 == other._stripGaindB100Layer3
@@ -51,8 +40,17 @@ class VBAN_VMRT_Packet_Data:
and self._stripGaindB100Layer7 == other._stripGaindB100Layer7 and self._stripGaindB100Layer7 == other._stripGaindB100Layer7
and self._stripGaindB100Layer8 == other._stripGaindB100Layer8 and self._stripGaindB100Layer8 == other._stripGaindB100Layer8
and self._busGaindB100 == other._busGaindB100 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 @property
def voicemeetertype(self) -> str: def voicemeetertype(self) -> str:
"""returns voicemeeter type as a string""" """returns voicemeeter type as a string"""
@@ -77,24 +75,14 @@ class VBAN_VMRT_Packet_Data:
return int.from_bytes(self._samplerate, "little") return int.from_bytes(self._samplerate, "little")
@property @property
def inputlevels(self) -> Generator[float, None, None]: def inputlevels(self) -> tuple:
"""returns the entire level array across all inputs""" """returns the entire level array across all inputs for a kind"""
for i in range(0, 68, 2): return self._strip_level[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
val = ((1 << 16) - 1) - int.from_bytes(
self._inputLeveldB100[i : i + 2], "little"
)
if val != ((1 << 16) - 1):
yield val
@property @property
def outputlevels(self) -> Generator[float, None, None]: def outputlevels(self) -> tuple:
"""returns the entire level array across all outputs""" """returns the entire level array across all outputs for a kind"""
for i in range(0, 128, 2): return self._bus_level[0 : 8 * self._kind.num_bus]
val = ((1 << 16) - 1) - int.from_bytes(
self._outputLeveldB100[i : i + 2], "little"
)
if val != ((1 << 16) - 1):
yield val
@property @property
def stripstate(self) -> tuple: def stripstate(self) -> tuple:
@@ -114,64 +102,56 @@ class VBAN_VMRT_Packet_Data:
@property @property
def stripgainlayer1(self) -> tuple: def stripgainlayer1(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer2(self) -> tuple: def stripgainlayer2(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer3(self) -> tuple: def stripgainlayer3(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer4(self) -> tuple: def stripgainlayer4(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer5(self) -> tuple: def stripgainlayer5(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer6(self) -> tuple: def stripgainlayer6(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer7(self) -> tuple: def stripgainlayer7(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@property @property
def stripgainlayer8(self) -> tuple: def stripgainlayer8(self) -> tuple:
return 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) for i in range(0, 16, 2)
) )
@@ -179,7 +159,7 @@ class VBAN_VMRT_Packet_Data:
def busgain(self) -> tuple: def busgain(self) -> tuple:
"""returns tuple of bus gains""" """returns tuple of bus gains"""
return tuple( 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) for i in range(0, 16, 2)
) )
@@ -201,8 +181,8 @@ class VBAN_VMRT_Packet_Data:
@dataclass @dataclass
class VBAN_VMRT_Packet_Header: class VbanRtPacketHeader:
"""Represents a RESPONSE RT PACKET header""" """Represents the header of VBAN RT data packet"""
name = "Voicemeeter-RTP" name = "Voicemeeter-RTP"
vban: bytes = "VBAN".encode() vban: bytes = "VBAN".encode()
@@ -225,7 +205,7 @@ class VBAN_VMRT_Packet_Header:
@dataclass @dataclass
class TextRequestHeader: class RequestHeader:
"""Represents a REQUEST RT PACKET header""" """Represents a REQUEST RT PACKET header"""
name: str name: str
@@ -262,8 +242,8 @@ class TextRequestHeader:
@dataclass @dataclass
class RegisterRTHeader: class SubscribeHeader:
"""Represents a REGISTER RT PACKET header""" """Represents a packet used to subscribe to the RT Packet Service"""
name = "Register RTP" name = "Register RTP"
timeout = 15 timeout = 15

View File

@@ -1,3 +1,4 @@
import time
from abc import abstractmethod from abc import abstractmethod
from typing import Union from typing import Union
@@ -40,6 +41,14 @@ class Strip(IRemote):
def gain(self, val: float): def gain(self, val: float):
self.setter("gain", val) self.setter("gain", val)
def fadeto(self, target: float, time_: int):
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): class PhysicalStrip(Strip):
def __str__(self): def __str__(self):
@@ -86,6 +95,12 @@ class VirtualStrip(Strip):
def k(self, val: int): def k(self, val: int):
self.setter("karaoke", val) 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): class StripLevel(IRemote):
def __init__(self, remote, index): def __init__(self, remote, index):
@@ -103,9 +118,23 @@ class StripLevel(IRemote):
self.range = self.level_map[self.index] self.range = self.level_map[self.index]
def getter(self): 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( return tuple(
round(-i * 0.01, 1) fget(i)
for i in self._remote.cache["strip_level"][self.range[0] : self.range[-1]] for i in self._remote.cache["strip_level"][
self.range[0] : self.range[-1]
]
)
return tuple(
fget(i)
for i in self._remote._get_levels(self.public_packet)[0][
self.range[0] : self.range[-1]
]
) )
@property @property
@@ -149,17 +178,12 @@ class GainLayer(IRemote):
def gain(self) -> float: def gain(self) -> float:
def fget(): def fget():
val = getattr(self.public_packet, f"stripgainlayer{self._i+1}")[self.index] val = getattr(self.public_packet, f"stripgainlayer{self._i+1}")[self.index]
if val < 10000: if 0 <= val <= 1200:
return -val return val * 0.01
elif val == ((1 << 16) - 1): return (((1 << 16) - 1) - val) * -0.01
return 0
else:
return ((1 << 16) - 1) - val
val = self.getter(f"GainLayer[{self._i}]") val = self.getter(f"GainLayer[{self._i}]")
if val is None: return round(val if val else fget(), 1)
val = fget() * 0.01
return round(val, 1)
@gain.setter @gain.setter
def gain(self, val: float): def gain(self, val: float):

View File

@@ -1,6 +1,11 @@
import logging
class Subject: class Subject:
"""Adds support for observers""" """Adds support for observers"""
logger = logging.getLogger("subject.subject")
def __init__(self): def __init__(self):
"""list of current observers""" """list of current observers"""
@@ -22,16 +27,26 @@ class Subject:
if observer not in self._observers: if observer not in self._observers:
self._observers.append(observer) self._observers.append(observer)
self.logger.info(f"{type(observer).__name__} added to event observers")
else: 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): def remove(self, observer):
"""removes an observer from _observers""" """removes an observer from _observers"""
try: try:
self._observers.remove(observer) self._observers.remove(observer)
self.logger.info(f"{type(observer).__name__} removed from event observers")
except ValueError: 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): def clear(self):
"""clears the _observers list""" """clears the _observers list"""

View File

@@ -10,6 +10,8 @@ def cache_bool(func, param):
cmd = f"{self.identifier}.{param}" cmd = f"{self.identifier}.{param}"
if cmd in self._remote.cache: if cmd in self._remote.cache:
return self._remote.cache.pop(cmd) == 1 return self._remote.cache.pop(cmd) == 1
if self._remote.sync:
self._remote.clear_dirty()
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
@@ -23,6 +25,8 @@ def cache_string(func, param):
cmd = f"{self.identifier}.{param}" cmd = f"{self.identifier}.{param}"
if cmd in self._remote.cache: if cmd in self._remote.cache:
return self._remote.cache.pop(cmd) return self._remote.cache.pop(cmd)
if self._remote.sync:
self._remote.clear_dirty()
return func(*args, **kwargs) return func(*args, **kwargs)
return wrapper return wrapper
@@ -61,9 +65,11 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
Evaluates equality of each member in both tuples. Evaluates equality of each member in both tuples.
""" """
for a, b in zip(t0, t1): for a, b in zip(t0, t1):
if b <= 9500: if ((1 << 16) - 1) - b <= 7200:
yield a == b yield a == b
else:
yield True yield True

View File

@@ -1,17 +1,24 @@
import logging
import socket import socket
import time import time
from abc import ABCMeta, abstractmethod 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 try:
from .packet import TextRequestHeader import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from .event import Event
from .packet import RequestHeader
from .subject import Subject from .subject import Subject
from .util import Socket, comp, script from .util import Socket, script
from .worker import Subscriber, Updater from .worker import Subscriber, Updater
class VbanCmd(metaclass=ABCMeta): 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 DELAY = 0.001
# fmt: off # fmt: off
@@ -21,12 +28,17 @@ class VbanCmd(metaclass=ABCMeta):
1000000, 1500000, 2000000, 3000000, 1000000, 1500000, 2000000, 3000000,
] ]
# fmt: on # fmt: on
logger = logging.getLogger("vbancmd.vbancmd")
def __init__(self, **kwargs): def __init__(self, **kwargs):
for attr, val in kwargs.items(): for attr, val in kwargs.items():
setattr(self, attr, val) 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, name=self.streamname,
bps_index=self.BPS_OPTS.index(self.bps), bps_index=self.BPS_OPTS.index(self.bps),
channel=self.channel, channel=self.channel,
@@ -35,14 +47,22 @@ class VbanCmd(metaclass=ABCMeta):
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
) )
self.subject = Subject() self.subject = Subject()
self.cache = dict() self.cache = {}
self.event = Event(self.subs) self.event = Event(self.subs)
self._pdirty = False
self._ldirty = False
@abstractmethod @abstractmethod
def __str__(self): def __str__(self):
"""Ensure subclasses override str magic method""" """Ensure subclasses override str magic method"""
pass 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): def __enter__(self):
self.login() self.login()
return self return self
@@ -50,6 +70,7 @@ class VbanCmd(metaclass=ABCMeta):
def login(self): def login(self):
"""Starts the subscriber and updater threads""" """Starts the subscriber and updater threads"""
self.running = True self.running = True
self.event.info()
self.subscriber = Subscriber(self) self.subscriber = Subscriber(self)
self.subscriber.start() self.subscriber.start()
@@ -57,6 +78,8 @@ class VbanCmd(metaclass=ABCMeta):
self.updater = Updater(self) self.updater = Updater(self)
self.updater.start() self.updater.start()
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
def _set_rt( def _set_rt(
self, self,
id_: str, id_: str,
@@ -64,17 +87,15 @@ class VbanCmd(metaclass=ABCMeta):
val: Optional[Union[int, float]] = None, val: Optional[Union[int, float]] = None,
): ):
"""Sends a string request command over a network.""" """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.socks[Socket.request].sendto(
self.text_header.header + cmd.encode(), self.packet_request.header + cmd.encode(),
(socket.gethostbyname(self.ip), self.port), (socket.gethostbyname(self.ip), self.port),
) )
count = int.from_bytes(self.text_header.framecounter, "little") + 1 count = int.from_bytes(self.packet_request.framecounter, "little") + 1
self.text_header.framecounter = count.to_bytes(4, "little") self.packet_request.framecounter = count.to_bytes(4, "little")
if param: if param:
self.cache[f"{id_}.{param}"] = val self.cache[f"{id_}.{param}"] = val
if self.sync:
time.sleep(0.02)
@script @script
def sendtext(self, cmd): def sendtext(self, cmd):
@@ -90,8 +111,7 @@ class VbanCmd(metaclass=ABCMeta):
@property @property
def version(self) -> str: def version(self) -> str:
"""Returns Voicemeeter's version as a string""" """Returns Voicemeeter's version as a string"""
v1, v2, v3, v4 = self.public_packet.voicemeeterversion return "{0}.{1}.{2}.{3}".format(*self.public_packet.voicemeeterversion)
return f"{v1}.{v2}.{v3}.{v4}"
@property @property
def pdirty(self): def pdirty(self):
@@ -101,11 +121,7 @@ class VbanCmd(metaclass=ABCMeta):
@property @property
def ldirty(self): def ldirty(self):
"""True iff a level value has changed.""" """True iff a level value has changed."""
self._strip_comp, self._bus_comp = ( return self._ldirty
tuple(not x for x in comp(self.cache["strip_level"], self._strip_buf)),
tuple(not x for x in comp(self.cache["bus_level"], self._bus_buf)),
)
return any(any(l) for l in (self._strip_comp, self._bus_comp))
@property @property
def public_packet(self): def public_packet(self):
@@ -113,7 +129,7 @@ class VbanCmd(metaclass=ABCMeta):
def clear_dirty(self): def clear_dirty(self):
while self.pdirty: while self.pdirty:
pass time.sleep(self.DELAY)
def _get_levels(self, packet) -> Iterable: def _get_levels(self, packet) -> Iterable:
""" """
@@ -122,8 +138,8 @@ class VbanCmd(metaclass=ABCMeta):
strip levels in PREFADER mode. strip levels in PREFADER mode.
""" """
return ( return (
tuple(val for val in packet.inputlevels), packet.inputlevels,
tuple(val for val in packet.outputlevels), packet.outputlevels,
) )
def apply(self, data: dict): def apply(self, data: dict):
@@ -151,14 +167,15 @@ class VbanCmd(metaclass=ABCMeta):
) )
try: try:
self.apply(self.configs[name]) self.apply(self.configs[name])
print(f"Profile '{name}' applied!") self.logger.info(f"Profile '{name}' applied!")
except KeyError as e: except KeyError as e:
print(("\n").join(error_msg)) self.logger.error(("\n").join(error_msg))
def logout(self): def logout(self):
self.running = False self.running = False
time.sleep(0.2) time.sleep(0.2)
[sock.close() for sock in self.socks] [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): def __exit__(self, exc_type, exc_value, exc_traceback):
self.logout() self.logout()

View File

@@ -1,40 +1,36 @@
import logging
import socket import socket
import threading import threading
import time import time
from enum import IntEnum
from typing import Optional from typing import Optional
from .packet import ( from .error import VBANCMDError
HEADER_SIZE, from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
RegisterRTHeader, from .util import Socket, comp
VBAN_VMRT_Packet_Data,
VBAN_VMRT_Packet_Header,
)
from .util import Socket
class Subscriber(threading.Thread): class Subscriber(threading.Thread):
"""fire a subscription packet every 10 seconds""" """fire a subscription packet every 10 seconds"""
def __init__(self, remote): def __init__(self, remote):
super().__init__(name="subscriber", target=self.register, daemon=True) super().__init__(name="subscriber", target=self.subscribe, daemon=True)
self._rem = remote self._remote = remote
self.register_header = RegisterRTHeader() self.packet = SubscribeHeader()
def register(self): def subscribe(self):
while self._rem.running: while self._remote.running:
try: try:
self._rem.socks[Socket.register].sendto( self._remote.socks[Socket.register].sendto(
self.register_header.header, self.packet.header,
(socket.gethostbyname(self._rem.ip), self._rem.port), (socket.gethostbyname(self._remote.ip), self._remote.port),
) )
count = int.from_bytes(self.register_header.framecounter, "little") + 1 count = int.from_bytes(self.packet.framecounter, "little") + 1
self.register_header.framecounter = count.to_bytes(4, "little") self.packet.framecounter = count.to_bytes(4, "little")
time.sleep(10) time.sleep(10)
except socket.gaierror as e: except socket.gaierror:
print(f"Unable to resolve hostname {self._rem.ip}") err_msg = f"Unable to resolve hostname {self._remote.ip}"
self._rem.socks[Socket.register].close() print(err_msg)
raise e raise VBANCMDError(err_msg)
class Updater(threading.Thread): class Updater(threading.Thread):
@@ -44,23 +40,35 @@ class Updater(threading.Thread):
notifies observers of event updates notifies observers of event updates
""" """
logger = logging.getLogger("worker.updater")
def __init__(self, remote): def __init__(self, remote):
super().__init__(name="updater", target=self.update, daemon=True) super().__init__(name="updater", target=self.update, daemon=True)
self._rem = remote self._remote = remote
self._rem.socks[Socket.response].bind( self._remote.socks[Socket.response].settimeout(5)
(socket.gethostbyname(socket.gethostname()), self._rem.port) self._remote.socks[Socket.response].bind(
(socket.gethostbyname(socket.gethostname()), self._remote.port)
) )
self.expected_packet = VBAN_VMRT_Packet_Header() self.packet_expected = VbanRtPacketHeader()
self._rem._public_packet = self._get_rt() self._remote._public_packet = self._get_rt()
(
self._remote.cache["strip_level"],
self._remote.cache["bus_level"],
) = self._remote._get_levels(self._remote.public_packet)
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]: def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
"""Returns a valid RT Data Packet or None""" try:
data, _ = self._rem.socks[Socket.response].recvfrom(2048) data, _ = self._remote.socks[Socket.response].recvfrom(2048)
# check for packet data # check for packet data
if len(data) > HEADER_SIZE: if len(data) > HEADER_SIZE:
# check if packet is of type VBAN # check if packet is of type rt packet response
if self.expected_packet.header == data[: HEADER_SIZE - 4]: if self.packet_expected.header == data[: HEADER_SIZE - 4]:
return VBAN_VMRT_Packet_Data( self.logger.debug("valid packet received")
return VbanRtPacket(
_kind=self._remote.kind,
_voicemeeterType=data[28:29], _voicemeeterType=data[28:29],
_reserved=data[29:30], _reserved=data[29:30],
_buffersize=data[30:32], _buffersize=data[30:32],
@@ -84,40 +92,51 @@ class Updater(threading.Thread):
_stripLabelUTF8c60=data[452:932], _stripLabelUTF8c60=data[452:932],
_busLabelUTF8c60=data[932:1412], _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""" """Attempt to fetch data packet until a valid one found"""
def fget(): def fget():
data = False data = None
while not data: while not data:
data = self._fetch_rt_packet() data = self._fetch_rt_packet()
time.sleep(self._rem.DELAY) time.sleep(self._remote.DELAY)
return data return data
return fget() return fget()
def update(self): def update(self):
print(f"Listening for {', '.join(self._rem.event.get())} events") while self._remote.running:
(
self._rem.cache["strip_level"],
self._rem.cache["bus_level"],
) = self._rem._get_levels(self._rem.public_packet)
while self._rem.running:
start = time.time() start = time.time()
_pp = self._get_rt() _pp = self._get_rt()
self._rem._strip_buf, self._rem._bus_buf = self._rem._get_levels(_pp) pdirty = _pp.pdirty(self._remote.public_packet)
self._rem._pdirty = _pp.pdirty(self._rem.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 elapsed = time.time() - start
if self._rem.ratelimit - elapsed > 0: if self._remote.ratelimit - elapsed > 0:
time.sleep(self._rem.ratelimit - elapsed) time.sleep(self._remote.ratelimit - elapsed)