mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-15 19:43:30 +00:00
Compare commits
19 Commits
8e30c57020
...
add-event-
| Author | SHA1 | Date | |
|---|---|---|---|
| cbcca14481 | |||
| f584d53835 | |||
| 72d182a488 | |||
| ee32f92914 | |||
| 3b65035e50 | |||
| c8b4bde49d | |||
| 47e9203b1e | |||
| d48e7ecd79 | |||
| 7e09a0d321 | |||
| d41ee1a12a | |||
| 1e499cd99d | |||
| 9bf52b5c11 | |||
| 77ba347e99 | |||
| 94fa33cebf | |||
| ef105d878b | |||
| 956f759e73 | |||
| dab519be9f | |||
| a4b91bf5c6 | |||
| 2a98707bf8 |
23
CHANGELOG.md
23
CHANGELOG.md
@@ -11,6 +11,29 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
|
## [2.3.2] - 2023-07-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- vban.{instream,outstream} tuples now contain classes that represent MIDI and TEXT streams.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- apply_config() now performs a deep merge when extending a config with another.
|
||||||
|
|
||||||
|
## [2.3.0] - 2023-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- user configs may now extend other user configs. check `config extends` section in README.
|
||||||
|
|
||||||
|
## [2.2.0] - 2023-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- button, vban classes implemented
|
||||||
|
- \__repr\__() method added to base class
|
||||||
|
|
||||||
## [2.1.2] - 2023-07-05
|
## [2.1.2] - 2023-07-05
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
55
README.md
55
README.md
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
# VBAN CMD
|
# VBAN CMD
|
||||||
|
|
||||||
This python interface allows you to get and set Voicemeeter parameter values over a network.
|
This python interface allows you to transmit Voicemeeter parameters over a network.
|
||||||
|
|
||||||
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
|
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ port = 6980
|
|||||||
streamname = "Command1"
|
streamname = "Command1"
|
||||||
```
|
```
|
||||||
|
|
||||||
It should be placed next to your `__main__.py` file.
|
It should be placed in \<user home directory\> / "Documents" / "Voicemeeter" / "configs"
|
||||||
|
|
||||||
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
||||||
|
|
||||||
@@ -251,7 +251,6 @@ The following properties are available.
|
|||||||
example:
|
example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.bus[4].eq = true
|
|
||||||
print(vban.bus[0].label)
|
print(vban.bus[0].label)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -262,6 +261,10 @@ The following properties are available.
|
|||||||
- `on`: boolean
|
- `on`: boolean
|
||||||
- `ab`: boolean
|
- `ab`: boolean
|
||||||
|
|
||||||
|
```python
|
||||||
|
vban.bus[4].eq.on = true
|
||||||
|
```
|
||||||
|
|
||||||
##### Modes
|
##### Modes
|
||||||
|
|
||||||
The following properties are available.
|
The following properties are available.
|
||||||
@@ -359,8 +362,8 @@ vban.apply(
|
|||||||
Or for each class you may do:
|
Or for each class you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.strip[0].apply(mute: true, gain: 3.2, A1: true)
|
vban.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||||
vban.bus[0].apply(A1: true)
|
vban.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config Files
|
## Config Files
|
||||||
@@ -369,7 +372,7 @@ vban.bus[0].apply(A1: true)
|
|||||||
|
|
||||||
You may load config files in TOML format.
|
You may load config files in TOML format.
|
||||||
Three example configs have been included with the package. Remember to save
|
Three example configs have been included with the package. Remember to save
|
||||||
current settings before loading a user config. To set one you may do:
|
current settings before loading a user config. To load one you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
@@ -379,6 +382,27 @@ 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.
|
||||||
|
|
||||||
|
Your configs may be located in one of the following paths:
|
||||||
|
- \<current working directory\> / "configs" / kind_id
|
||||||
|
- \<user home directory\> / ".config" / "vban-cmd" / kind_id
|
||||||
|
- \<user home directory\> / "Documents" / "Voicemeeter" / "configs" / kind_id
|
||||||
|
|
||||||
|
If a config with the same name is located in multiple locations, only the first one found is loaded into memory, in the above order.
|
||||||
|
|
||||||
|
#### `config extends`
|
||||||
|
|
||||||
|
You may also load a config that extends another config with overrides or additional parameters.
|
||||||
|
|
||||||
|
You just need to define a key `extends` in the config TOML, that names the config to be extended.
|
||||||
|
|
||||||
|
Three example 'extender' configs are included with the repo. You may load them with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import voicemeeterlib
|
||||||
|
with voicemeeterlib.api('banana') as vm:
|
||||||
|
vm.apply_config('extender')
|
||||||
|
```
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
||||||
@@ -485,12 +509,27 @@ Returns a `VbanRtPacket`. Designed to be used internally by the interface but av
|
|||||||
|
|
||||||
States not guaranteed to be current (requires use of dirty parameters to confirm).
|
States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||||
|
|
||||||
### `Errors`
|
## Errors
|
||||||
|
|
||||||
- `errors.VBANCMDError`: Exception raised when general errors occur.
|
- `errors.VBANCMDError`: Exception raised when general errors occur.
|
||||||
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
||||||
|
|
||||||
### `Tests`
|
## Logging
|
||||||
|
|
||||||
|
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||||
|
|
||||||
|
example:
|
||||||
|
```python
|
||||||
|
import vban_cmd
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
opts = {"ip": "ip.local", "port": 6980, "streamname": "Command1"}
|
||||||
|
with vban_cmd.api('banana', **opts) as vban:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
||||||
|
|
||||||
|
|||||||
12
configs/banana/extender.toml
Normal file
12
configs/banana/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
12
configs/basic/extender.toml
Normal file
12
configs/basic/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
12
configs/potato/extender.toml
Normal file
12
configs/potato/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
@@ -8,6 +8,8 @@ from tkinter import ttk
|
|||||||
|
|
||||||
|
|
||||||
class App(tk.Tk):
|
class App(tk.Tk):
|
||||||
|
INDEX = 3
|
||||||
|
|
||||||
def __init__(self, vban):
|
def __init__(self, vban):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.vban = vban
|
self.vban = vban
|
||||||
@@ -15,8 +17,8 @@ class App(tk.Tk):
|
|||||||
self.vban.observer.add(self.on_ldirty)
|
self.vban.observer.add(self.on_ldirty)
|
||||||
|
|
||||||
# create widget variables
|
# create widget variables
|
||||||
self.button_var = tk.BooleanVar(value=vban.strip[3].mute)
|
self.button_var = tk.BooleanVar(value=vban.strip[self.INDEX].mute)
|
||||||
self.slider_var = tk.DoubleVar(value=vban.strip[3].gain)
|
self.slider_var = tk.DoubleVar(value=vban.strip[self.INDEX].gain)
|
||||||
self.meter_var = tk.DoubleVar(value=self._get_level())
|
self.meter_var = tk.DoubleVar(value=self._get_level())
|
||||||
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
|
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
|
||||||
|
|
||||||
@@ -24,11 +26,12 @@ class App(tk.Tk):
|
|||||||
self.style = ttk.Style()
|
self.style = ttk.Style()
|
||||||
self.style.theme_use("clam")
|
self.style.theme_use("clam")
|
||||||
self.style.configure(
|
self.style.configure(
|
||||||
"Mute.TButton", foreground="#cd5c5c" if vban.strip[3].mute else "#5a5a5a"
|
"Mute.TButton",
|
||||||
|
foreground="#cd5c5c" if vban.strip[self.INDEX].mute else "#5a5a5a",
|
||||||
)
|
)
|
||||||
|
|
||||||
# create labelframe and grid it onto the mainframe
|
# create labelframe and grid it onto the mainframe
|
||||||
self.labelframe = tk.LabelFrame(text=self.vban.strip[3].label)
|
self.labelframe = tk.LabelFrame(text=self.vban.strip[self.INDEX].label)
|
||||||
self.labelframe.grid(padx=1)
|
self.labelframe.grid(padx=1)
|
||||||
|
|
||||||
# create slider and grid it onto the labelframe
|
# create slider and grid it onto the labelframe
|
||||||
@@ -44,6 +47,7 @@ class App(tk.Tk):
|
|||||||
column=0,
|
column=0,
|
||||||
row=0,
|
row=0,
|
||||||
)
|
)
|
||||||
|
slider.bind("<Double-Button-1>", self.on_button_double_click)
|
||||||
|
|
||||||
# create level meter and grid it onto the labelframe
|
# create level meter and grid it onto the labelframe
|
||||||
level_meter = ttk.Progressbar(
|
level_meter = ttk.Progressbar(
|
||||||
@@ -72,18 +76,23 @@ class App(tk.Tk):
|
|||||||
|
|
||||||
def on_slider_move(self, *args):
|
def on_slider_move(self, *args):
|
||||||
val = round(self.slider_var.get(), 1)
|
val = round(self.slider_var.get(), 1)
|
||||||
self.vban.strip[3].gain = val
|
self.vban.strip[self.INDEX].gain = val
|
||||||
self.gainlabel_var.set(val)
|
self.gainlabel_var.set(val)
|
||||||
|
|
||||||
def on_button_press(self):
|
def on_button_press(self):
|
||||||
self.button_var.set(not self.button_var.get())
|
self.button_var.set(not self.button_var.get())
|
||||||
self.vban.strip[3].mute = self.button_var.get()
|
self.vban.strip[self.INDEX].mute = self.button_var.get()
|
||||||
self.style.configure(
|
self.style.configure(
|
||||||
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def on_button_double_click(self, e):
|
||||||
|
self.slider_var.set(0)
|
||||||
|
self.gainlabel_var.set(0)
|
||||||
|
self.vban.strip[self.INDEX].gain = 0
|
||||||
|
|
||||||
def _get_level(self):
|
def _get_level(self):
|
||||||
val = max(self.vban.strip[3].levels.prefader)
|
val = max(self.vban.strip[self.INDEX].levels.prefader)
|
||||||
return 0 if self.button_var.get() else 72 + val - 12 + self.slider_var.get()
|
return 0 if self.button_var.get() else 72 + val - 12 + self.slider_var.get()
|
||||||
|
|
||||||
def on_ldirty(self):
|
def on_ldirty(self):
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vban-cmd"
|
name = "vban-cmd"
|
||||||
version = "2.2.0"
|
version = "2.4.3"
|
||||||
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"
|
||||||
|
|||||||
@@ -14,9 +14,13 @@ class TestRemoteFactories:
|
|||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(vban.strip) == 3
|
assert len(vban.strip) == 3
|
||||||
assert len(vban.bus) == 2
|
assert len(vban.bus) == 2
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 6 and len(vban.vban.outstream) == 5
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
@@ -26,9 +30,13 @@ class TestRemoteFactories:
|
|||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(vban.strip) == 5
|
assert len(vban.strip) == 5
|
||||||
assert len(vban.bus) == 5
|
assert len(vban.bus) == 5
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
@@ -38,6 +46,10 @@ class TestRemoteFactories:
|
|||||||
assert hasattr(vban, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(vban, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(vban, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(vban.strip) == 8
|
assert len(vban.strip) == 8
|
||||||
assert len(vban.bus) == 8
|
assert len(vban.bus) == 8
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ class BusLevel(IRemote):
|
|||||||
def fget(i):
|
def fget(i):
|
||||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||||
|
|
||||||
if self._remote.running and self._remote.event.ldirty:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return tuple(
|
||||||
fget(i)
|
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]]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
class VBANCMDError(Exception):
|
class VBANCMDError(Exception):
|
||||||
"""Exception raised when general errors occur"""
|
"""Base VBANCMD Exception class. Raised when general errors occur"""
|
||||||
|
|
||||||
|
|
||||||
class VBANCMDConnectionError(Exception):
|
class VBANCMDConnectionError(VBANCMDError):
|
||||||
"""Exception raised when connection/timeout errors occur"""
|
"""Exception raised when connection/timeout errors occur"""
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ 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
|
from typing import Iterable
|
||||||
|
|
||||||
from .bus import request_bus_obj as bus
|
from .bus import request_bus_obj as bus
|
||||||
from .command import Command
|
from .command import Command
|
||||||
@@ -41,7 +41,7 @@ class FactoryBuilder:
|
|||||||
)
|
)
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
def _pinfo(self, name: str) -> NoReturn:
|
def _pinfo(self, name: str) -> None:
|
||||||
"""prints progress status for each step"""
|
"""prints progress status for each step"""
|
||||||
name = name.split("_")[1]
|
name = name.split("_")[1]
|
||||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
||||||
|
|||||||
@@ -110,6 +110,7 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
cmd += (f".{param}",)
|
cmd += (f".{param}",)
|
||||||
return "".join(cmd)
|
return "".join(cmd)
|
||||||
|
|
||||||
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -53,6 +53,14 @@ class KindMapClass(metaclass=SingletonType):
|
|||||||
def num_bus(self):
|
def num_bus(self):
|
||||||
return sum(self.outs)
|
return sum(self.outs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_strip_levels(self) -> int:
|
||||||
|
return 2 * self.phys_in + 8 * self.virt_in
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_bus_levels(self) -> int:
|
||||||
|
return 8 * (self.phys_out + self.virt_out)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name.capitalize()
|
return self.name.capitalize()
|
||||||
|
|
||||||
@@ -62,7 +70,7 @@ class BasicMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (2, 1)
|
ins: tuple = (2, 1)
|
||||||
outs: tuple = (1, 1)
|
outs: tuple = (1, 1)
|
||||||
vban: tuple = (4, 4)
|
vban: tuple = (4, 4, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -70,7 +78,7 @@ class BananaMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (3, 2)
|
ins: tuple = (3, 2)
|
||||||
outs: tuple = (3, 2)
|
outs: tuple = (3, 2)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -78,7 +86,7 @@ class PotatoMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (5, 3)
|
ins: tuple = (5, 3)
|
||||||
outs: tuple = (5, 3)
|
outs: tuple = (5, 3)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def kind_factory(kind_id):
|
def kind_factory(kind_id):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ class MacroButton(IRemote):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
return f"button[{self.index}]"
|
return f"command.button[{self.index}]"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self) -> bool:
|
def state(self) -> bool:
|
||||||
|
|||||||
@@ -3,10 +3,14 @@ from dataclasses import dataclass
|
|||||||
from .kinds import KindMapClass
|
from .kinds import KindMapClass
|
||||||
from .util import comp
|
from .util import comp
|
||||||
|
|
||||||
|
VBAN_PROTOCOL_TXT = 0x40
|
||||||
|
VBAN_PROTOCOL_SERVICE = 0x60
|
||||||
|
|
||||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||||
VBAN_SERVICE_RTPACKET = 33
|
VBAN_SERVICE_RTPACKET = 33
|
||||||
|
|
||||||
MAX_PACKET_SIZE = 1436
|
MAX_PACKET_SIZE = 1436
|
||||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -14,28 +18,28 @@ class VbanRtPacket:
|
|||||||
"""Represents the body of a VBAN RT data packet"""
|
"""Represents the body of a VBAN RT data packet"""
|
||||||
|
|
||||||
_kind: KindMapClass
|
_kind: KindMapClass
|
||||||
_voicemeeterType: bytes
|
_voicemeeterType: bytes # data[28:29]
|
||||||
_reserved: bytes
|
_reserved: bytes # data[29:30]
|
||||||
_buffersize: bytes
|
_buffersize: bytes # data[30:32]
|
||||||
_voicemeeterVersion: bytes
|
_voicemeeterVersion: bytes # data[32:36]
|
||||||
_optionBits: bytes
|
_optionBits: bytes # data[36:40]
|
||||||
_samplerate: bytes
|
_samplerate: bytes # data[40:44]
|
||||||
_inputLeveldB100: bytes
|
_inputLeveldB100: bytes # data[44:112]
|
||||||
_outputLeveldB100: bytes
|
_outputLeveldB100: bytes # data[112:240]
|
||||||
_TransportBit: bytes
|
_TransportBit: bytes # data[240:244]
|
||||||
_stripState: bytes
|
_stripState: bytes # data[244:276]
|
||||||
_busState: bytes
|
_busState: bytes # data[276:308]
|
||||||
_stripGaindB100Layer1: bytes
|
_stripGaindB100Layer1: bytes # data[308:324]
|
||||||
_stripGaindB100Layer2: bytes
|
_stripGaindB100Layer2: bytes # data[324:340]
|
||||||
_stripGaindB100Layer3: bytes
|
_stripGaindB100Layer3: bytes # data[340:356]
|
||||||
_stripGaindB100Layer4: bytes
|
_stripGaindB100Layer4: bytes # data[356:372]
|
||||||
_stripGaindB100Layer5: bytes
|
_stripGaindB100Layer5: bytes # data[372:388]
|
||||||
_stripGaindB100Layer6: bytes
|
_stripGaindB100Layer6: bytes # data[388:404]
|
||||||
_stripGaindB100Layer7: bytes
|
_stripGaindB100Layer7: bytes # data[404:420]
|
||||||
_stripGaindB100Layer8: bytes
|
_stripGaindB100Layer8: bytes # data[420:436]
|
||||||
_busGaindB100: bytes
|
_busGaindB100: bytes # data[436:452]
|
||||||
_stripLabelUTF8c60: bytes
|
_stripLabelUTF8c60: bytes # data[452:932]
|
||||||
_busLabelUTF8c60: bytes
|
_busLabelUTF8c60: bytes # data[932:1412]
|
||||||
|
|
||||||
def _generate_levels(self, levelarray) -> tuple:
|
def _generate_levels(self, levelarray) -> tuple:
|
||||||
return tuple(
|
return tuple(
|
||||||
@@ -103,12 +107,12 @@ class VbanRtPacket:
|
|||||||
@property
|
@property
|
||||||
def inputlevels(self) -> tuple:
|
def inputlevels(self) -> tuple:
|
||||||
"""returns the entire level array across all inputs for a kind"""
|
"""returns the entire level array across all inputs for a kind"""
|
||||||
return self.strip_levels[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
|
return self.strip_levels[0 : self._kind.num_strip_levels]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def outputlevels(self) -> tuple:
|
def outputlevels(self) -> tuple:
|
||||||
"""returns the entire level array across all outputs for a kind"""
|
"""returns the entire level array across all outputs for a kind"""
|
||||||
return self.bus_levels[0 : 8 * self._kind.num_bus]
|
return self.bus_levels[0 : self._kind.num_bus_levels]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stripstate(self) -> tuple:
|
def stripstate(self) -> tuple:
|
||||||
@@ -206,13 +210,42 @@ class VbanRtPacket:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubscribeHeader:
|
||||||
|
"""Represents the header an RT Packet Service subscription packet"""
|
||||||
|
|
||||||
|
name = "Register RTP"
|
||||||
|
timeout = 15
|
||||||
|
vban: bytes = "VBAN".encode()
|
||||||
|
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||||
|
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||||
|
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
||||||
|
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
||||||
|
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
||||||
|
framecounter: bytes = (0).to_bytes(4, "little")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header(self):
|
||||||
|
header = self.vban
|
||||||
|
header += self.format_sr
|
||||||
|
header += self.format_nbs
|
||||||
|
header += self.format_nbc
|
||||||
|
header += self.format_bit
|
||||||
|
header += self.streamname
|
||||||
|
header += self.framecounter
|
||||||
|
assert (
|
||||||
|
len(header) == HEADER_SIZE + 4
|
||||||
|
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VbanRtPacketHeader:
|
class VbanRtPacketHeader:
|
||||||
"""Represents the header of VBAN RT data packet"""
|
"""Represents the header of a VBAN RT response packet"""
|
||||||
|
|
||||||
name = "Voicemeeter-RTP"
|
name = "Voicemeeter-RTP"
|
||||||
vban: bytes = "VBAN".encode()
|
vban: bytes = "VBAN".encode()
|
||||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
||||||
format_bit: bytes = (0).to_bytes(1, "little")
|
format_bit: bytes = (0).to_bytes(1, "little")
|
||||||
@@ -226,13 +259,13 @@ class VbanRtPacketHeader:
|
|||||||
header += self.format_nbc
|
header += self.format_nbc
|
||||||
header += self.format_bit
|
header += self.format_bit
|
||||||
header += self.streamname
|
header += self.streamname
|
||||||
assert len(header) == HEADER_SIZE - 4, f"Header expected {HEADER_SIZE-4} bytes"
|
assert len(header) == HEADER_SIZE, f"expected header size {HEADER_SIZE} bytes"
|
||||||
return header
|
return header
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RequestHeader:
|
class RequestHeader:
|
||||||
"""Represents a REQUEST RT PACKET header"""
|
"""Represents the header of an REQUEST RT PACKET"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
bps_index: int
|
bps_index: int
|
||||||
@@ -244,7 +277,7 @@ class RequestHeader:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sr(self):
|
def sr(self):
|
||||||
return (0x40 + self.bps_index).to_bytes(1, "little")
|
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, "little")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nbc(self):
|
def nbc(self):
|
||||||
@@ -263,32 +296,7 @@ class RequestHeader:
|
|||||||
header += self.bit
|
header += self.bit
|
||||||
header += self.streamname
|
header += self.streamname
|
||||||
header += self.framecounter
|
header += self.framecounter
|
||||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
assert (
|
||||||
return header
|
len(header) == HEADER_SIZE + 4
|
||||||
|
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SubscribeHeader:
|
|
||||||
"""Represents a packet used to subscribe to the RT Packet Service"""
|
|
||||||
|
|
||||||
name = "Register RTP"
|
|
||||||
timeout = 15
|
|
||||||
vban: bytes = "VBAN".encode()
|
|
||||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
|
||||||
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
|
||||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
|
||||||
framecounter: bytes = (0).to_bytes(4, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def header(self):
|
|
||||||
header = self.vban
|
|
||||||
header += self.format_sr
|
|
||||||
header += self.format_nbs
|
|
||||||
header += self.format_nbc
|
|
||||||
header += self.format_bit
|
|
||||||
header += self.streamname
|
|
||||||
header += self.framecounter
|
|
||||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
|
||||||
return header
|
return header
|
||||||
|
|||||||
@@ -296,7 +296,7 @@ class StripLevel(IRemote):
|
|||||||
def fget(i):
|
def fget(i):
|
||||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||||
|
|
||||||
if self._remote.running and self._remote.event.ldirty:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return tuple(
|
||||||
fget(i)
|
fget(i)
|
||||||
for i in self._remote.cache["strip_level"][
|
for i in self._remote.cache["strip_level"][
|
||||||
|
|||||||
@@ -73,4 +73,18 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
|||||||
yield True
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
def deep_merge(dict1, dict2):
|
||||||
|
"""Generator function for deep merging two dicts"""
|
||||||
|
for k in set(dict1) | set(dict2):
|
||||||
|
if k in dict1 and k in dict2:
|
||||||
|
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
|
||||||
|
yield k, dict(deep_merge(dict1[k], dict2[k]))
|
||||||
|
else:
|
||||||
|
yield k, dict2[k]
|
||||||
|
elif k in dict1:
|
||||||
|
yield k, dict1[k]
|
||||||
|
else:
|
||||||
|
yield k, dict2[k]
|
||||||
|
|
||||||
|
|
||||||
Socket = IntEnum("Socket", "register request response", start=0)
|
Socket = IntEnum("Socket", "register request response", start=0)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
|
from .kinds import kinds_all
|
||||||
|
|
||||||
|
|
||||||
class VbanStream(IRemote):
|
class VbanStream(IRemote):
|
||||||
@@ -133,6 +134,18 @@ class VbanInstream(VbanStream):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class VbanAudioInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Audio Instream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanMidiInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Midi Instream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanTextInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Text Instream"""
|
||||||
|
|
||||||
|
|
||||||
class VbanOutstream(VbanStream):
|
class VbanOutstream(VbanStream):
|
||||||
"""
|
"""
|
||||||
class representing a vban outstream
|
class representing a vban outstream
|
||||||
@@ -148,6 +161,50 @@ class VbanOutstream(VbanStream):
|
|||||||
return "out"
|
return "out"
|
||||||
|
|
||||||
|
|
||||||
|
class VbanAudioOutstream(VbanOutstream):
|
||||||
|
"""Represents a VBAN Audio Outstream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanMidiOutstream(VbanOutstream):
|
||||||
|
"""Represents a VBAN Midi Outstream"""
|
||||||
|
|
||||||
|
|
||||||
|
def _make_stream_pair(remote, kind):
|
||||||
|
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||||
|
|
||||||
|
def _generate_streams(i, dir):
|
||||||
|
"""generator function for creating instream/outstream tuples"""
|
||||||
|
if dir == "in":
|
||||||
|
if i < num_instream:
|
||||||
|
yield VbanAudioInstream
|
||||||
|
elif i < num_instream + num_midi:
|
||||||
|
yield VbanMidiInstream
|
||||||
|
else:
|
||||||
|
yield VbanTextInstream
|
||||||
|
else:
|
||||||
|
if i < num_outstream:
|
||||||
|
yield VbanAudioOutstream
|
||||||
|
else:
|
||||||
|
yield VbanMidiOutstream
|
||||||
|
|
||||||
|
return (
|
||||||
|
tuple(
|
||||||
|
cls(remote, i)
|
||||||
|
for i in range(num_instream + num_midi + num_text)
|
||||||
|
for cls in _generate_streams(i, "in")
|
||||||
|
),
|
||||||
|
tuple(
|
||||||
|
cls(remote, i)
|
||||||
|
for i in range(num_outstream + num_midi)
|
||||||
|
for cls in _generate_streams(i, "out")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_stream_pairs(remote):
|
||||||
|
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||||
|
|
||||||
|
|
||||||
class Vban:
|
class Vban:
|
||||||
"""
|
"""
|
||||||
class representing the vban module
|
class representing the vban module
|
||||||
@@ -157,9 +214,7 @@ class Vban:
|
|||||||
|
|
||||||
def __init__(self, remote):
|
def __init__(self, remote):
|
||||||
self.remote = remote
|
self.remote = remote
|
||||||
num_instream, num_outstream = remote.kind.vban
|
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||||
self.instream = tuple(VbanInstream(remote, i) for i in range(num_instream))
|
|
||||||
self.outstream = tuple(VbanOutstream(remote, i) for i in range(num_outstream))
|
|
||||||
|
|
||||||
def enable(self):
|
def enable(self):
|
||||||
"""if VBAN disabled there can be no communication with it"""
|
"""if VBAN disabled there can be no communication with it"""
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from queue import Queue
|
from queue import Queue
|
||||||
from typing import Iterable, Optional, Union
|
from typing import Iterable, Union
|
||||||
|
|
||||||
from .error import VBANCMDError
|
from .error import VBANCMDError
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .packet import RequestHeader
|
from .packet import RequestHeader
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
from .util import Socket, script
|
from .util import Socket, deep_merge, script
|
||||||
from .worker import Producer, Subscriber, Updater
|
from .worker import Producer, Subscriber, Updater
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -64,8 +65,9 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
def get_filepath():
|
def get_filepath():
|
||||||
filepaths = [
|
filepaths = [
|
||||||
Path.cwd() / "vban.toml",
|
Path.cwd() / "vban.toml",
|
||||||
|
Path.cwd() / "configs" / "vban.toml",
|
||||||
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
||||||
Path.home() / "Documents" / "Voicemeeter" / "vban.toml",
|
Path.home() / "Documents" / "Voicemeeter" / "configs" / "vban.toml",
|
||||||
]
|
]
|
||||||
for filepath in filepaths:
|
for filepath in filepaths:
|
||||||
if filepath.exists():
|
if filepath.exists():
|
||||||
@@ -75,37 +77,40 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
with open(filepath, "rb") as f:
|
with open(filepath, "rb") as f:
|
||||||
conn = tomllib.load(f)
|
conn = tomllib.load(f)
|
||||||
assert (
|
assert (
|
||||||
"ip" in conn["connection"]
|
"connection" in conn and "ip" in conn["connection"]
|
||||||
), "please provide ip, by kwarg or config"
|
), "expected [connection][ip] in vban config"
|
||||||
return conn["connection"]
|
return conn["connection"]
|
||||||
else:
|
|
||||||
raise VBANCMDError("no ip provided and no vban.toml located.")
|
raise VBANCMDError("no ip provided and no vban.toml located.")
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.login()
|
self.login()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def login(self):
|
def login(self) -> None:
|
||||||
"""Starts the subscriber and updater threads (unless in outbound mode)"""
|
"""Starts the subscriber and updater threads (unless in outbound mode)"""
|
||||||
if not self.outbound:
|
if not self.outbound:
|
||||||
self.running = True
|
|
||||||
self.event.info()
|
self.event.info()
|
||||||
|
|
||||||
self.subscriber = Subscriber(self)
|
self.stop_event = threading.Event()
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.subscriber = Subscriber(self, self.stop_event)
|
||||||
self.subscriber.start()
|
self.subscriber.start()
|
||||||
|
|
||||||
queue = Queue()
|
queue = Queue()
|
||||||
self.updater = Updater(self, queue)
|
self.updater = Updater(self, queue)
|
||||||
self.updater.start()
|
self.updater.start()
|
||||||
self.producer = Producer(self, queue)
|
self.producer = Producer(self, queue, self.stop_event)
|
||||||
self.producer.start()
|
self.producer.start()
|
||||||
|
|
||||||
self.logger.info(
|
self.logger.info(
|
||||||
"Successfully logged into {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format(
|
"Successfully logged into VBANCMD {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format(
|
||||||
**self.__dict__
|
**self.__dict__
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def _set_rt(self, cmd: str, val: Union[str, float]):
|
def _set_rt(self, cmd: str, val: Union[str, float]):
|
||||||
"""Sends a string request command over a network."""
|
"""Sends a string request command over a network."""
|
||||||
self.socks[Socket.request].sendto(
|
self.socks[Socket.request].sendto(
|
||||||
@@ -154,7 +159,7 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
def public_packet(self):
|
def public_packet(self):
|
||||||
return self._public_packet
|
return self._public_packet
|
||||||
|
|
||||||
def clear_dirty(self):
|
def clear_dirty(self) -> None:
|
||||||
while self.pdirty:
|
while self.pdirty:
|
||||||
time.sleep(self.DELAY)
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
@@ -189,22 +194,36 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
|
|
||||||
def apply_config(self, name):
|
def apply_config(self, name):
|
||||||
"""applies a config from memory"""
|
"""applies a config from memory"""
|
||||||
error_msg = (
|
ERR_MSG = (
|
||||||
f"No config with name '{name}' is loaded into memory",
|
f"No config with name '{name}' is loaded into memory",
|
||||||
f"Known configs: {list(self.configs.keys())}",
|
f"Known configs: {list(self.configs.keys())}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.apply(self.configs[name])
|
config = self.configs[name]
|
||||||
self.logger.info(f"Profile '{name}' applied!")
|
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self.logger.error(("\n").join(error_msg))
|
self.logger.error(("\n").join(ERR_MSG))
|
||||||
raise VBANCMDError(("\n").join(error_msg)) from e
|
raise VBANCMDError(("\n").join(ERR_MSG)) from e
|
||||||
|
|
||||||
def logout(self):
|
if "extends" in config:
|
||||||
self.running = False
|
extended = config["extends"]
|
||||||
time.sleep(0.2)
|
config = {
|
||||||
|
k: v
|
||||||
|
for k, v in deep_merge(self.configs[extended], config)
|
||||||
|
if k not in ("extends")
|
||||||
|
}
|
||||||
|
self.logger.debug(
|
||||||
|
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||||
|
)
|
||||||
|
self.apply(config)
|
||||||
|
self.logger.info(f"Profile '{name}' applied!")
|
||||||
|
|
||||||
|
def logout(self) -> None:
|
||||||
|
if not self.stopped():
|
||||||
|
self.logger.debug("events thread shutdown started")
|
||||||
|
self.stop_event.set()
|
||||||
|
self.subscriber.join() # wait for subscriber thread to complete cycle
|
||||||
[sock.close() for sock in self.socks]
|
[sock.close() for sock in self.socks]
|
||||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
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) -> None:
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ logger = logging.getLogger(__name__)
|
|||||||
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, stop_event):
|
||||||
super().__init__(name="subscriber", daemon=True)
|
super().__init__(name="subscriber", daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
|
self.stop_event = stop_event
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.packet = SubscribeHeader()
|
self.packet = SubscribeHeader()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._remote.running:
|
while not self.stopped():
|
||||||
try:
|
try:
|
||||||
self._remote.socks[Socket.register].sendto(
|
self._remote.socks[Socket.register].sendto(
|
||||||
self.packet.header,
|
self.packet.header,
|
||||||
@@ -30,23 +31,39 @@ class Subscriber(threading.Thread):
|
|||||||
self.packet.framecounter = (
|
self.packet.framecounter = (
|
||||||
int.from_bytes(self.packet.framecounter, "little") + 1
|
int.from_bytes(self.packet.framecounter, "little") + 1
|
||||||
).to_bytes(4, "little")
|
).to_bytes(4, "little")
|
||||||
time.sleep(10)
|
self.wait_until_stopped(10)
|
||||||
except socket.gaierror as e:
|
except socket.gaierror as e:
|
||||||
self.logger.exception(f"{type(e).__name__}: {e}")
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise VBANCMDConnectionError(
|
raise VBANCMDConnectionError(
|
||||||
f"unable to resolve hostname {self._remote.ip}"
|
f"unable to resolve hostname {self._remote.ip}"
|
||||||
) from e
|
) from e
|
||||||
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
|
def wait_until_stopped(self, timeout, period=0.2):
|
||||||
|
must_end = time.time() + timeout
|
||||||
|
while time.time() < must_end:
|
||||||
|
if self.stopped():
|
||||||
|
break
|
||||||
|
time.sleep(period)
|
||||||
|
|
||||||
|
|
||||||
class Producer(threading.Thread):
|
class Producer(threading.Thread):
|
||||||
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||||
|
|
||||||
def __init__(self, remote, queue):
|
def __init__(self, remote, queue, stop_event):
|
||||||
super().__init__(name="producer", daemon=True)
|
super().__init__(name="producer", daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
|
self.stop_event = stop_event
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.packet_expected = VbanRtPacketHeader()
|
self.packet_expected = VbanRtPacketHeader()
|
||||||
|
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
||||||
|
self._remote.socks[Socket.response].bind(
|
||||||
|
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||||
|
)
|
||||||
self._remote._public_packet = self._get_rt()
|
self._remote._public_packet = self._get_rt()
|
||||||
(
|
(
|
||||||
self._remote.cache["strip_level"],
|
self._remote.cache["strip_level"],
|
||||||
@@ -60,7 +77,6 @@ class Producer(threading.Thread):
|
|||||||
data = None
|
data = None
|
||||||
while not data:
|
while not data:
|
||||||
data = self._fetch_rt_packet()
|
data = self._fetch_rt_packet()
|
||||||
time.sleep(self._remote.DELAY)
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
return fget()
|
return fget()
|
||||||
@@ -68,10 +84,10 @@ class Producer(threading.Thread):
|
|||||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||||
try:
|
try:
|
||||||
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
||||||
# check for packet data
|
# do we have packet data?
|
||||||
if len(data) > HEADER_SIZE:
|
if len(data) > HEADER_SIZE:
|
||||||
# check if packet is of type rt packet response
|
# is the packet of type VBAN RT response?
|
||||||
if self.packet_expected.header == data[: HEADER_SIZE - 4]:
|
if self.packet_expected.header == data[:HEADER_SIZE]:
|
||||||
return VbanRtPacket(
|
return VbanRtPacket(
|
||||||
_kind=self._remote.kind,
|
_kind=self._remote.kind,
|
||||||
_voicemeeterType=data[28:29],
|
_voicemeeterType=data[28:29],
|
||||||
@@ -103,8 +119,11 @@ class Producer(threading.Thread):
|
|||||||
f"timeout waiting for RtPacket from {self._remote.ip}"
|
f"timeout waiting for RtPacket from {self._remote.ip}"
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._remote.running:
|
while not self.stopped():
|
||||||
_pp = self._get_rt()
|
_pp = self._get_rt()
|
||||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||||
ldirty = _pp.ldirty(
|
ldirty = _pp.ldirty(
|
||||||
@@ -137,13 +156,8 @@ class Updater(threading.Thread):
|
|||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.logger = logger.getChild(self.__class__.__name__)
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||||
self._remote.socks[Socket.response].bind(
|
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
|
||||||
)
|
|
||||||
p_in, v_in = self._remote.kind.ins
|
|
||||||
self._remote._strip_comp = [False] * (2 * p_in + 8 * v_in)
|
|
||||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
"""
|
"""
|
||||||
@@ -151,12 +165,7 @@ class Updater(threading.Thread):
|
|||||||
|
|
||||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||||
"""
|
"""
|
||||||
while True:
|
while event := self.queue.get():
|
||||||
event = self.queue.get()
|
|
||||||
if event is None:
|
|
||||||
self.logger.debug(f"terminating {self.name} thread")
|
|
||||||
break
|
|
||||||
|
|
||||||
if event == "pdirty" and self._remote.pdirty:
|
if event == "pdirty" and self._remote.pdirty:
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
elif event == "ldirty" and self._remote.ldirty:
|
elif event == "ldirty" and self._remote.ldirty:
|
||||||
@@ -172,3 +181,4 @@ class Updater(threading.Thread):
|
|||||||
self._remote._public_packet.outputlevels,
|
self._remote._public_packet.outputlevels,
|
||||||
)
|
)
|
||||||
self._remote.subject.notify(event)
|
self._remote.subject.notify(event)
|
||||||
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
|
|||||||
Reference in New Issue
Block a user