mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-03-03 00:39:10 +00:00
Compare commits
No commits in common. "a8ef82166c0e136d8f950fa2e2c0d34246a0734b" and "91feccc5090960126e6756d65e30b5b003472179" have entirely different histories.
a8ef82166c
...
91feccc509
53
.github/workflows/publish.yml
vendored
53
.github/workflows/publish.yml
vendored
@ -1,53 +0,0 @@
|
||||
name: Publish to PyPI
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pip install poetry==2.3.1
|
||||
poetry --version
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
poetry install --only-root
|
||||
poetry build
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
pypi-publish:
|
||||
needs: build
|
||||
name: Upload release to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/project/vban-cmd/
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: dist
|
||||
path: ./dist
|
||||
|
||||
- name: Publish package distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
packages-dir: ./dist
|
||||
19
.github/workflows/ruff.yml
vendored
19
.github/workflows/ruff.yml
vendored
@ -1,19 +0,0 @@
|
||||
name: Ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: 'format --check --diff'
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -151,8 +151,8 @@ cython_debug/
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# test files
|
||||
test-*.py
|
||||
# quick test
|
||||
quick.py
|
||||
|
||||
#config
|
||||
config.toml
|
||||
|
||||
@ -11,13 +11,6 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [2.6.0] - 2026-02-26
|
||||
|
||||
### Added
|
||||
|
||||
- support for packet with [ident:1](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/3be2c1c36563afbd6df3da8436406c77d2cc1f10/VoicemeeterRemote.h#L982) in VBAN TEXT subprotocol.
|
||||
- This includes Strip 3D, PEQ, comp, gate, denoiser and pitch parameters.
|
||||
|
||||
## [2.5.2] - 2025-01-25
|
||||
|
||||
### Changed
|
||||
|
||||
51
README.md
51
README.md
@ -171,7 +171,9 @@ example:
|
||||
print(vban.strip[4].comp.knob)
|
||||
```
|
||||
|
||||
Strip Comp `knob` is defined for all versions, all other parameters potato only.
|
||||
Strip Comp properties are defined as write only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Gate
|
||||
|
||||
@ -191,7 +193,9 @@ example:
|
||||
vban.strip[2].gate.attack = 300.8
|
||||
```
|
||||
|
||||
Strip Gate `knob` is defined for all versions, all other parameters potato only.
|
||||
Strip Gate properties are defined as write only, potato version only.
|
||||
|
||||
`knob` defined for all versions, all other parameters potato only.
|
||||
|
||||
##### Strip.Denoiser
|
||||
|
||||
@ -208,32 +212,7 @@ The following properties are available.
|
||||
- `on`: boolean
|
||||
- `ab`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].eq.ab = True
|
||||
```
|
||||
|
||||
##### Strip.EQ.Channel.Cell
|
||||
|
||||
The following properties are available.
|
||||
|
||||
- `on`: boolean
|
||||
- `type`: int, from 0 up to 6
|
||||
- `f`: float, from 20.0 up to 20_000.0
|
||||
- `gain`: float, from -36.0 up to 18.0
|
||||
- `q`: float, from 0.3 up to 100
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vban.strip[0].eq.channel[0].cell[2].on = True
|
||||
vban.strip[1].eq.channel[0].cell[2].f = 5000
|
||||
```
|
||||
|
||||
Strip EQ parameters are defined for PhysicalStrips, potato version only.
|
||||
|
||||
Only channel[0] properties are readable over VBAN.
|
||||
Strip EQ properties are defined as write only, potato version only.
|
||||
|
||||
##### Gainlayers
|
||||
|
||||
@ -421,8 +400,8 @@ You just need to define a key `extends` in the config TOML, that names the confi
|
||||
Three example 'extender' configs are included with the repo. You may load them with:
|
||||
|
||||
```python
|
||||
import vban_cmd
|
||||
with vban_cmd.api('banana') as vm:
|
||||
import voicemeeterlib
|
||||
with voicemeeterlib.api('banana') as vm:
|
||||
vm.apply_config('extender')
|
||||
```
|
||||
|
||||
@ -549,15 +528,13 @@ with vban_cmd.api('banana', **opts) as vban:
|
||||
...
|
||||
```
|
||||
|
||||
### Run tests
|
||||
## Tests
|
||||
|
||||
Install [poetry](https://python-poetry.org/docs/#installation) and then:
|
||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
||||
|
||||
```powershell
|
||||
poetry poe test-basic
|
||||
poetry poe test-banana
|
||||
poetry poe test-potato
|
||||
```
|
||||
Then from tests directory:
|
||||
|
||||
`pytest -v`
|
||||
|
||||
## Resources
|
||||
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
import tkinter as tk
|
||||
from tkinter import ttk
|
||||
|
||||
@ -101,14 +100,7 @@ class App(tk.Tk):
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = 'banana'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, ldirty=True, **conn) as vban:
|
||||
with vban_cmd.api('banana', ldirty=True) as vban:
|
||||
app = App(vban)
|
||||
app.mainloop()
|
||||
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import os
|
||||
import threading
|
||||
from logging import config
|
||||
|
||||
@ -93,13 +92,8 @@ class Observer:
|
||||
|
||||
def main():
|
||||
KIND_ID = 'potato'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, **conn) as vban:
|
||||
with vban_cmd.api(KIND_ID) as vban:
|
||||
stop_event = threading.Event()
|
||||
|
||||
with Observer(vban, stop_event):
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
import os
|
||||
|
||||
import vban_cmd
|
||||
|
||||
@ -24,13 +23,8 @@ class App:
|
||||
|
||||
def main():
|
||||
KIND_ID = 'banana'
|
||||
conn = {
|
||||
'ip': os.environ.get('VBANCMD_IP', 'localhost'),
|
||||
'port': int(os.environ.get('VBANCMD_PORT', 6980)),
|
||||
'streamname': os.environ.get('VBANCMD_STREAMNAME', 'Command1'),
|
||||
}
|
||||
|
||||
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True, **conn) as vban:
|
||||
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True) as vban:
|
||||
App(vban)
|
||||
|
||||
while _ := input('Press <Enter> to exit\n'):
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
[project]
|
||||
name = "vban-cmd"
|
||||
version = "2.6.0"
|
||||
version = "2.5.2"
|
||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||
license = { text = "MIT" }
|
||||
authors = [
|
||||
{name = "Onyx and Iris",email = "code@onyxandiris.online"}
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = ["tomli (>=2.0.1,<3.0) ; python_version < '3.11'"]
|
||||
dependencies = [
|
||||
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
|
||||
]
|
||||
|
||||
[tool.poetry.requires-plugins]
|
||||
poethepoet = "^0.35.0"
|
||||
poethepoet = "^0.32.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^8.3.4"
|
||||
@ -22,22 +26,19 @@ virtualenv-pyenv = "^0.5.0"
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe]
|
||||
envfile = ".env"
|
||||
|
||||
[tool.poe.tasks]
|
||||
gui.script = "scripts:ex_gui"
|
||||
obs.script = "scripts:ex_obs"
|
||||
observer.script = "scripts:ex_observer"
|
||||
test-basic.script = "scripts:test_basic"
|
||||
test-banana.script = "scripts:test_banana"
|
||||
test-potato.script = "scripts:test_potato"
|
||||
test-all.script = "scripts:test_all"
|
||||
test_basic.script = "scripts:test_basic"
|
||||
test_banana.script = "scripts:test_banana"
|
||||
test_potato.script = "scripts:test_potato"
|
||||
test_all.script = "scripts:test_all"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310,py311,py312,py313
|
||||
envlist = py310,py311,py312
|
||||
|
||||
[testenv]
|
||||
passenv = *
|
||||
@ -135,4 +136,7 @@ docstring-code-line-length = "dynamic"
|
||||
max-complexity = 10
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["E402", "F401"]
|
||||
"__init__.py" = [
|
||||
"E402",
|
||||
"F401",
|
||||
]
|
||||
|
||||
@ -11,9 +11,9 @@ from vban_cmd.kinds import request_kind_map as kindmap
|
||||
KIND_ID = os.environ.get('KIND', 'potato')
|
||||
|
||||
opts = {
|
||||
'ip': os.getenv('VBANCMD_IP', 'localhost'),
|
||||
'streamname': os.getenv('VBANCMD_STREAMNAME', 'Command1'),
|
||||
'port': int(os.getenv('VBANCMD_PORT', 6980)),
|
||||
'ip': 'localhost',
|
||||
'streamname': 'onyx',
|
||||
'port': 6980,
|
||||
}
|
||||
|
||||
vban = vban_cmd.api(KIND_ID, **opts)
|
||||
|
||||
@ -176,7 +176,6 @@ class TestSetAndGetFloatHigher:
|
||||
|
||||
""" strip tests, virtual """
|
||||
|
||||
@pytest.mark.skip(reason='Requires RT Packet NBS 1')
|
||||
@pytest.mark.parametrize(
|
||||
'index, param, value',
|
||||
[
|
||||
|
||||
@ -9,9 +9,9 @@ class TestPublicPacketLower:
|
||||
|
||||
"""Tests for a valid rt data packet"""
|
||||
|
||||
def test_it_gets_an_rt0_data_packet(self):
|
||||
assert vban.public_packets[0].voicemeetertype in (
|
||||
kind.name for kind in kinds.all
|
||||
def test_it_gets_an_rt_data_packet(self):
|
||||
assert vban.public_packet.voicemeetertype in (
|
||||
kind.name for kind in kinds.kinds_all
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import abc
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from typing import Union
|
||||
|
||||
from .enums import NBS, BusModes
|
||||
from .iremote import IRemote
|
||||
from .meta import bus_mode_prop, channel_bool_prop, channel_label_prop
|
||||
|
||||
BusModes = IntEnum(
|
||||
'BusModes',
|
||||
'normal amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly',
|
||||
start=0,
|
||||
)
|
||||
|
||||
|
||||
class Bus(IRemote):
|
||||
"""
|
||||
@ -14,7 +20,7 @@ class Bus(IRemote):
|
||||
Defines concrete implementation for bus
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@ -24,11 +30,14 @@ class Bus(IRemote):
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = self.public_packet.busgain[self.index]
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter('gain')
|
||||
if val:
|
||||
return round(val, 2)
|
||||
else:
|
||||
return self.public_packets[NBS.zero].busgain[self.index]
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
@ -100,7 +109,7 @@ class BusLevel(IRemote):
|
||||
)
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packets[NBS.zero])[1][
|
||||
for i in self._remote._get_levels(self.public_packet)[1][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
@ -148,12 +157,7 @@ def _make_bus_mode_mixin():
|
||||
|
||||
def get(self):
|
||||
states = [
|
||||
(
|
||||
int.from_bytes(
|
||||
self.public_packets[NBS.zero].busstate[self.index], 'little'
|
||||
)
|
||||
& val
|
||||
)
|
||||
(int.from_bytes(self.public_packet.busstate[self.index], 'little') & val)
|
||||
>> 4
|
||||
for val in self._modes.modevals
|
||||
]
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
from enum import Enum, IntEnum, unique
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
BASIC = 1
|
||||
BANANA = 2
|
||||
POTATO = 3
|
||||
|
||||
|
||||
class NBS(IntEnum):
|
||||
zero = 0
|
||||
one = 1
|
||||
|
||||
|
||||
BusModes = IntEnum(
|
||||
'BusModes',
|
||||
'normal amix bmix repeat composite tvmix upmix21 upmix41 upmix61 centeronly lfeonly rearonly',
|
||||
start=0,
|
||||
)
|
||||
@ -1,5 +1,5 @@
|
||||
import abc
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from functools import cached_property
|
||||
from typing import Iterable
|
||||
@ -115,7 +115,7 @@ class FactoryBase(VbanCmd):
|
||||
)
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def steps(self):
|
||||
pass
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import abc
|
||||
import logging
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -78,7 +78,7 @@ class Modes:
|
||||
)
|
||||
|
||||
|
||||
class IRemote(abc.ABC):
|
||||
class IRemote(metaclass=ABCMeta):
|
||||
"""
|
||||
Common interface between base class and extended (higher) classes
|
||||
|
||||
@ -111,14 +111,14 @@ class IRemote(abc.ABC):
|
||||
return ''.join(cmd)
|
||||
|
||||
@property
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def public_packets(self):
|
||||
def public_packet(self):
|
||||
"""Returns an RT data packet."""
|
||||
return self._remote.public_packets
|
||||
return self._remote.public_packet
|
||||
|
||||
def apply(self, data):
|
||||
"""Sets all parameters of a dict for the channel."""
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum, unique
|
||||
|
||||
from .enums import KindId
|
||||
from .error import VBANCMDError
|
||||
|
||||
|
||||
@unique
|
||||
class KindId(Enum):
|
||||
BASIC = 1
|
||||
BANANA = 2
|
||||
POTATO = 3
|
||||
|
||||
|
||||
class SingletonType(type):
|
||||
"""ensure only a single instance of a kind map object"""
|
||||
|
||||
@ -15,15 +22,12 @@ class SingletonType(type):
|
||||
return cls._instances[cls]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class KindMapClass(metaclass=SingletonType):
|
||||
name: str
|
||||
ins: tuple
|
||||
outs: tuple
|
||||
vban: tuple
|
||||
strip_channels: int
|
||||
bus_channels: int
|
||||
cells: int
|
||||
|
||||
@property
|
||||
def phys_in(self):
|
||||
@ -61,37 +65,28 @@ class KindMapClass(metaclass=SingletonType):
|
||||
return self.name.capitalize()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class BasicMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (2, 1)
|
||||
outs: tuple = (1, 1)
|
||||
vban: tuple = (4, 4, 1, 1)
|
||||
strip_channels: int = 0
|
||||
bus_channels: int = 0
|
||||
cells: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class BananaMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (3, 2)
|
||||
outs: tuple = (3, 2)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
strip_channels: int = 0
|
||||
bus_channels: int = 8
|
||||
cells: int = 6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@dataclass
|
||||
class PotatoMap(KindMapClass):
|
||||
name: str
|
||||
ins: tuple = (5, 3)
|
||||
outs: tuple = (5, 3)
|
||||
vban: tuple = (8, 8, 1, 1)
|
||||
strip_channels: int = 2
|
||||
bus_channels: int = 8
|
||||
cells: int = 6
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
@ -116,4 +111,4 @@ def request_kind_map(kind_id):
|
||||
return KIND_obj
|
||||
|
||||
|
||||
all = list(request_kind_map(kind_id.name.lower()) for kind_id in KindId)
|
||||
kinds_all = list(request_kind_map(kind_id.name.lower()) for kind_id in KindId)
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
from functools import partial
|
||||
|
||||
from .enums import NBS
|
||||
from .util import cache_bool, cache_float, cache_string
|
||||
from .util import cache_bool, cache_string
|
||||
|
||||
|
||||
def channel_bool_prop(param):
|
||||
@ -14,7 +13,7 @@ def channel_bool_prop(param):
|
||||
return (
|
||||
not int.from_bytes(
|
||||
getattr(
|
||||
self.public_packets[NBS.zero],
|
||||
self.public_packet,
|
||||
f'{"strip" if "strip" in type(self).__name__.lower() else "bus"}state',
|
||||
)[self.index],
|
||||
'little',
|
||||
@ -35,7 +34,7 @@ def channel_label_prop():
|
||||
@partial(cache_string, param='label')
|
||||
def fget(self) -> str:
|
||||
return getattr(
|
||||
self.public_packets[NBS.zero],
|
||||
self.public_packet,
|
||||
f'{"strip" if "strip" in type(self).__name__.lower() else "bus"}labels',
|
||||
)[self.index]
|
||||
|
||||
@ -53,9 +52,7 @@ def strip_output_prop(param):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
return (
|
||||
not int.from_bytes(
|
||||
self.public_packets[NBS.zero].stripstate[self.index], 'little'
|
||||
)
|
||||
not int.from_bytes(self.public_packet.stripstate[self.index], 'little')
|
||||
& getattr(self._modes, f'_bus{param.lower()}')
|
||||
== 0
|
||||
)
|
||||
@ -74,12 +71,7 @@ def bus_mode_prop(param):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
return [
|
||||
(
|
||||
int.from_bytes(
|
||||
self.public_packets[NBS.zero].busstate[self.index], 'little'
|
||||
)
|
||||
& val
|
||||
)
|
||||
(int.from_bytes(self.public_packet.busstate[self.index], 'little') & val)
|
||||
>> 4
|
||||
for val in self._modes.modevals
|
||||
] == self.modestates[param]
|
||||
@ -97,61 +89,3 @@ def action_fn(param, val=1):
|
||||
self.setter(param, val)
|
||||
|
||||
return fdo
|
||||
|
||||
|
||||
def xy_prop(param):
|
||||
"""meta function for XY pad parameters"""
|
||||
|
||||
@partial(cache_float, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
|
||||
positions = self.public_packets[NBS.one].strips[self.index].positions
|
||||
match param:
|
||||
case 'pan_x':
|
||||
return positions.pan_x
|
||||
case 'pan_y':
|
||||
return positions.pan_y
|
||||
case 'color_x':
|
||||
return positions.color_x
|
||||
case 'color_y':
|
||||
return positions.color_y
|
||||
case 'fx1':
|
||||
return positions.fx1
|
||||
case 'fx2':
|
||||
return positions.fx2
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def send_prop(param):
|
||||
"""meta function for send parameters"""
|
||||
|
||||
@partial(cache_float, param=param)
|
||||
def fget(self):
|
||||
cmd = self._cmd(param)
|
||||
self.logger.debug(f'getter: {cmd}')
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
|
||||
sends = self.public_packets[NBS.one].strips[self.index].sends
|
||||
match param:
|
||||
case 'reverb':
|
||||
return sends.reverb
|
||||
case 'delay':
|
||||
return sends.delay
|
||||
case 'fx1':
|
||||
return sends.fx1
|
||||
case 'fx2':
|
||||
return sends.fx2
|
||||
|
||||
def fset(self, val):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
@ -1,8 +1,5 @@
|
||||
import struct
|
||||
from dataclasses import dataclass
|
||||
from typing import NamedTuple
|
||||
|
||||
from .enums import NBS
|
||||
from .kinds import KindMapClass
|
||||
from .util import comp
|
||||
|
||||
@ -11,76 +8,38 @@ VBAN_PROTOCOL_SERVICE = 0x60
|
||||
|
||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||
VBAN_SERVICE_RTPACKET = 33
|
||||
VBAN_SERVICE_MASK = 0xE0
|
||||
|
||||
MAX_PACKET_SIZE = 1436
|
||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||
VMPARAMSTRIP_SIZE = 174
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacket:
|
||||
"""Represents the body of a VBAN RT data packet"""
|
||||
|
||||
nbs: NBS
|
||||
_kind: KindMapClass
|
||||
_voicemeeterType: bytes
|
||||
_reserved: bytes
|
||||
_buffersize: bytes
|
||||
_voicemeeterVersion: bytes
|
||||
_optionBits: bytes
|
||||
_samplerate: bytes
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketNBS0(VbanRtPacket):
|
||||
"""Represents the body of a VBAN RT data packet with NBS 0"""
|
||||
|
||||
_inputLeveldB100: bytes
|
||||
_outputLeveldB100: bytes
|
||||
_TransportBit: bytes
|
||||
_stripState: bytes
|
||||
_busState: bytes
|
||||
_stripGaindB100Layer1: bytes
|
||||
_stripGaindB100Layer2: bytes
|
||||
_stripGaindB100Layer3: bytes
|
||||
_stripGaindB100Layer4: bytes
|
||||
_stripGaindB100Layer5: bytes
|
||||
_stripGaindB100Layer6: bytes
|
||||
_stripGaindB100Layer7: bytes
|
||||
_stripGaindB100Layer8: bytes
|
||||
_busGaindB100: bytes
|
||||
_stripLabelUTF8c60: bytes
|
||||
_busLabelUTF8c60: bytes
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, nbs: NBS, kind: KindMapClass, data: bytes):
|
||||
return cls(
|
||||
nbs=nbs,
|
||||
_kind=kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
_inputLeveldB100=data[44:112],
|
||||
_outputLeveldB100=data[112:240],
|
||||
_TransportBit=data[240:244],
|
||||
_stripState=data[244:276],
|
||||
_busState=data[276:308],
|
||||
_stripGaindB100Layer1=data[308:324],
|
||||
_stripGaindB100Layer2=data[324:340],
|
||||
_stripGaindB100Layer3=data[340:356],
|
||||
_stripGaindB100Layer4=data[356:372],
|
||||
_stripGaindB100Layer5=data[372:388],
|
||||
_stripGaindB100Layer6=data[388:404],
|
||||
_stripGaindB100Layer7=data[404:420],
|
||||
_stripGaindB100Layer8=data[420:436],
|
||||
_busGaindB100=data[436:452],
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
_voicemeeterType: bytes # data[28:29]
|
||||
_reserved: bytes # data[29:30]
|
||||
_buffersize: bytes # data[30:32]
|
||||
_voicemeeterVersion: bytes # data[32:36]
|
||||
_optionBits: bytes # data[36:40]
|
||||
_samplerate: bytes # data[40:44]
|
||||
_inputLeveldB100: bytes # data[44:112]
|
||||
_outputLeveldB100: bytes # data[112:240]
|
||||
_TransportBit: bytes # data[240:244]
|
||||
_stripState: bytes # data[244:276]
|
||||
_busState: bytes # data[276:308]
|
||||
_stripGaindB100Layer1: bytes # data[308:324]
|
||||
_stripGaindB100Layer2: bytes # data[324:340]
|
||||
_stripGaindB100Layer3: bytes # data[340:356]
|
||||
_stripGaindB100Layer4: bytes # data[356:372]
|
||||
_stripGaindB100Layer5: bytes # data[372:388]
|
||||
_stripGaindB100Layer6: bytes # data[388:404]
|
||||
_stripGaindB100Layer7: bytes # data[404:420]
|
||||
_stripGaindB100Layer8: bytes # data[420:436]
|
||||
_busGaindB100: bytes # data[436:452]
|
||||
_stripLabelUTF8c60: bytes # data[452:932]
|
||||
_busLabelUTF8c60: bytes # data[932:1412]
|
||||
|
||||
def _generate_levels(self, levelarray) -> tuple:
|
||||
return tuple(
|
||||
@ -171,33 +130,66 @@ class VbanRtPacketNBS0(VbanRtPacket):
|
||||
"""
|
||||
|
||||
@property
|
||||
def gainlayers(self) -> tuple:
|
||||
"""returns tuple of all strip gain layers as tuples"""
|
||||
def stripgainlayer1(self) -> tuple:
|
||||
return tuple(
|
||||
tuple(
|
||||
round(
|
||||
int.from_bytes(
|
||||
getattr(self, f'_stripGaindB100Layer{layer}')[i : i + 2],
|
||||
'little',
|
||||
signed=True,
|
||||
)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
int.from_bytes(self._stripGaindB100Layer1[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
for layer in range(1, 9)
|
||||
|
||||
@property
|
||||
def stripgainlayer2(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer2[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer3(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer3[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer4(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer4[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer5(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer5[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer6(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer6[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer7(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer7[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def stripgainlayer8(self) -> tuple:
|
||||
return tuple(
|
||||
int.from_bytes(self._stripGaindB100Layer8[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@property
|
||||
def busgain(self) -> tuple:
|
||||
"""returns tuple of bus gains"""
|
||||
return tuple(
|
||||
round(
|
||||
int.from_bytes(self._busGaindB100[i : i + 2], 'little', signed=True)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
int.from_bytes(self._busGaindB100[i : i + 2], 'little')
|
||||
for i in range(0, 16, 2)
|
||||
)
|
||||
|
||||
@ -218,480 +210,93 @@ class VbanRtPacketNBS0(VbanRtPacket):
|
||||
)
|
||||
|
||||
|
||||
class Audibility(NamedTuple):
|
||||
knob: float
|
||||
comp: float
|
||||
gate: float
|
||||
denoiser: float
|
||||
|
||||
|
||||
class Positions(NamedTuple):
|
||||
pan_x: float
|
||||
pan_y: float
|
||||
color_x: float
|
||||
color_y: float
|
||||
fx1: float
|
||||
fx2: float
|
||||
|
||||
|
||||
class EqGains(NamedTuple):
|
||||
bass: float
|
||||
mid: float
|
||||
treble: float
|
||||
|
||||
|
||||
class ParametricEQSettings(NamedTuple):
|
||||
on: bool
|
||||
type: int
|
||||
gain: float
|
||||
freq: float
|
||||
q: float
|
||||
|
||||
|
||||
class Sends(NamedTuple):
|
||||
reverb: float
|
||||
delay: float
|
||||
fx1: float
|
||||
fx2: float
|
||||
|
||||
|
||||
class CompressorSettings(NamedTuple):
|
||||
gain_in: float
|
||||
attack_ms: float
|
||||
release_ms: float
|
||||
n_knee: float
|
||||
ratio: float
|
||||
threshold: float
|
||||
c_enabled: bool
|
||||
makeup: bool
|
||||
gain_out: float
|
||||
|
||||
|
||||
class GateSettings(NamedTuple):
|
||||
threshold_in: float
|
||||
damping_max: float
|
||||
bp_sidechain: bool
|
||||
attack_ms: float
|
||||
hold_ms: float
|
||||
release_ms: float
|
||||
|
||||
|
||||
class DenoiserSettings(NamedTuple):
|
||||
threshold: float
|
||||
|
||||
|
||||
class PitchSettings(NamedTuple):
|
||||
enabled: bool
|
||||
dry_wet: float
|
||||
value: float
|
||||
formant_lo: float
|
||||
formant_med: float
|
||||
formant_high: float
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanVMParamStrip:
|
||||
"""Represents the VBAN_VMPARAMSTRIP_PACKET structure"""
|
||||
|
||||
_mode: bytes
|
||||
_dblevel: bytes
|
||||
_audibility: bytes
|
||||
_pos3D_x: bytes
|
||||
_pos3D_y: bytes
|
||||
_posColor_x: bytes
|
||||
_posColor_y: bytes
|
||||
_EQgain1: bytes
|
||||
_EQgain2: bytes
|
||||
_EQgain3: bytes
|
||||
|
||||
# First channel parametric EQ
|
||||
_PEQ_eqOn: bytes
|
||||
_PEQ_eqtype: bytes
|
||||
_PEQ_eqgain: bytes
|
||||
_PEQ_eqfreq: bytes
|
||||
_PEQ_eqq: bytes
|
||||
|
||||
_audibility_c: bytes
|
||||
_audibility_g: bytes
|
||||
_audibility_d: bytes
|
||||
_posMod_x: bytes
|
||||
_posMod_y: bytes
|
||||
_send_reverb: bytes
|
||||
_send_delay: bytes
|
||||
_send_fx1: bytes
|
||||
_send_fx2: bytes
|
||||
_dblimit: bytes
|
||||
_nKaraoke: bytes
|
||||
|
||||
_COMP_gain_in: bytes
|
||||
_COMP_attack_ms: bytes
|
||||
_COMP_release_ms: bytes
|
||||
_COMP_n_knee: bytes
|
||||
_COMP_comprate: bytes
|
||||
_COMP_threshold: bytes
|
||||
_COMP_c_enabled: bytes
|
||||
_COMP_c_auto: bytes
|
||||
_COMP_gain_out: bytes
|
||||
|
||||
_GATE_dBThreshold_in: bytes
|
||||
_GATE_dBDamping_max: bytes
|
||||
_GATE_BP_Sidechain: bytes
|
||||
_GATE_attack_ms: bytes
|
||||
_GATE_hold_ms: bytes
|
||||
_GATE_release_ms: bytes
|
||||
|
||||
_DenoiserThreshold: bytes
|
||||
_PitchEnabled: bytes
|
||||
_Pitch_DryWet: bytes
|
||||
_Pitch_Value: bytes
|
||||
_Pitch_formant_lo: bytes
|
||||
_Pitch_formant_med: bytes
|
||||
_Pitch_formant_high: bytes
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
return cls(
|
||||
_mode=data[0:4],
|
||||
_dblevel=data[4:8],
|
||||
_audibility=data[8:10],
|
||||
_pos3D_x=data[10:12],
|
||||
_pos3D_y=data[12:14],
|
||||
_posColor_x=data[14:16],
|
||||
_posColor_y=data[16:18],
|
||||
_EQgain1=data[18:20],
|
||||
_EQgain2=data[20:22],
|
||||
_EQgain3=data[22:24],
|
||||
_PEQ_eqOn=data[24:30],
|
||||
_PEQ_eqtype=data[30:36],
|
||||
_PEQ_eqgain=data[36:60],
|
||||
_PEQ_eqfreq=data[60:84],
|
||||
_PEQ_eqq=data[84:108],
|
||||
_audibility_c=data[108:110],
|
||||
_audibility_g=data[110:112],
|
||||
_audibility_d=data[112:114],
|
||||
_posMod_x=data[114:116],
|
||||
_posMod_y=data[116:118],
|
||||
_send_reverb=data[118:120],
|
||||
_send_delay=data[120:122],
|
||||
_send_fx1=data[122:124],
|
||||
_send_fx2=data[124:126],
|
||||
_dblimit=data[126:128],
|
||||
_nKaraoke=data[128:130],
|
||||
_COMP_gain_in=data[130:132],
|
||||
_COMP_attack_ms=data[132:134],
|
||||
_COMP_release_ms=data[134:136],
|
||||
_COMP_n_knee=data[136:138],
|
||||
_COMP_comprate=data[138:140],
|
||||
_COMP_threshold=data[140:142],
|
||||
_COMP_c_enabled=data[142:144],
|
||||
_COMP_c_auto=data[144:146],
|
||||
_COMP_gain_out=data[146:148],
|
||||
_GATE_dBThreshold_in=data[148:150],
|
||||
_GATE_dBDamping_max=data[150:152],
|
||||
_GATE_BP_Sidechain=data[152:154],
|
||||
_GATE_attack_ms=data[154:156],
|
||||
_GATE_hold_ms=data[156:158],
|
||||
_GATE_release_ms=data[158:160],
|
||||
_DenoiserThreshold=data[160:162],
|
||||
_PitchEnabled=data[162:164],
|
||||
_Pitch_DryWet=data[164:166],
|
||||
_Pitch_Value=data[166:168],
|
||||
_Pitch_formant_lo=data[168:170],
|
||||
_Pitch_formant_med=data[170:172],
|
||||
_Pitch_formant_high=data[172:174],
|
||||
)
|
||||
|
||||
@property
|
||||
def mode(self) -> int:
|
||||
return int.from_bytes(self._mode, 'little')
|
||||
|
||||
@property
|
||||
def audibility(self) -> Audibility:
|
||||
return Audibility(
|
||||
round(int.from_bytes(self._audibility, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_c, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_g, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._audibility_d, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def positions(self) -> Positions:
|
||||
return Positions(
|
||||
round(int.from_bytes(self._pos3D_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._pos3D_y, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posColor_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posColor_y, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posMod_x, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._posMod_y, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def eqgains(self) -> EqGains:
|
||||
return EqGains(
|
||||
*[
|
||||
round(
|
||||
int.from_bytes(getattr(self, f'_EQgain{i}'), 'little', signed=True)
|
||||
* 0.01,
|
||||
2,
|
||||
)
|
||||
for i in range(1, 4)
|
||||
]
|
||||
)
|
||||
|
||||
@property
|
||||
def parametric_eq(self) -> tuple[ParametricEQSettings, ...]:
|
||||
return tuple(
|
||||
ParametricEQSettings(
|
||||
on=bool(int.from_bytes(self._PEQ_eqOn[i : i + 1], 'little')),
|
||||
type=int.from_bytes(self._PEQ_eqtype[i : i + 1], 'little'),
|
||||
freq=struct.unpack('<f', self._PEQ_eqfreq[i * 4 : (i + 1) * 4])[0],
|
||||
gain=struct.unpack('<f', self._PEQ_eqgain[i * 4 : (i + 1) * 4])[0],
|
||||
q=struct.unpack('<f', self._PEQ_eqq[i * 4 : (i + 1) * 4])[0],
|
||||
)
|
||||
for i in range(6)
|
||||
)
|
||||
|
||||
@property
|
||||
def sends(self) -> Sends:
|
||||
return Sends(
|
||||
round(int.from_bytes(self._send_reverb, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_delay, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_fx1, 'little', signed=True) * 0.01, 2),
|
||||
round(int.from_bytes(self._send_fx2, 'little', signed=True) * 0.01, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def karaoke(self) -> int:
|
||||
return int.from_bytes(self._nKaraoke, 'little')
|
||||
|
||||
@property
|
||||
def compressor(self) -> CompressorSettings:
|
||||
return CompressorSettings(
|
||||
gain_in=round(
|
||||
int.from_bytes(self._COMP_gain_in, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
attack_ms=round(int.from_bytes(self._COMP_attack_ms, 'little') * 0.1, 2),
|
||||
release_ms=round(int.from_bytes(self._COMP_release_ms, 'little') * 0.1, 2),
|
||||
n_knee=round(int.from_bytes(self._COMP_n_knee, 'little') * 0.01, 2),
|
||||
ratio=round(int.from_bytes(self._COMP_comprate, 'little') * 0.01, 2),
|
||||
threshold=round(
|
||||
int.from_bytes(self._COMP_threshold, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
c_enabled=bool(int.from_bytes(self._COMP_c_enabled, 'little')),
|
||||
makeup=bool(int.from_bytes(self._COMP_c_auto, 'little')),
|
||||
gain_out=round(
|
||||
int.from_bytes(self._COMP_gain_out, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
)
|
||||
|
||||
@property
|
||||
def gate(self) -> GateSettings:
|
||||
return GateSettings(
|
||||
threshold_in=round(
|
||||
int.from_bytes(self._GATE_dBThreshold_in, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
damping_max=round(
|
||||
int.from_bytes(self._GATE_dBDamping_max, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
bp_sidechain=round(
|
||||
int.from_bytes(self._GATE_BP_Sidechain, 'little') * 0.1, 2
|
||||
),
|
||||
attack_ms=round(int.from_bytes(self._GATE_attack_ms, 'little') * 0.1, 2),
|
||||
hold_ms=round(int.from_bytes(self._GATE_hold_ms, 'little') * 0.1, 2),
|
||||
release_ms=round(int.from_bytes(self._GATE_release_ms, 'little') * 0.1, 2),
|
||||
)
|
||||
|
||||
@property
|
||||
def denoiser(self) -> DenoiserSettings:
|
||||
return DenoiserSettings(
|
||||
threshold=round(
|
||||
int.from_bytes(self._DenoiserThreshold, 'little', signed=True) * 0.01, 2
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def pitch(self) -> PitchSettings:
|
||||
return PitchSettings(
|
||||
enabled=bool(int.from_bytes(self._PitchEnabled, 'little')),
|
||||
dry_wet=round(
|
||||
int.from_bytes(self._Pitch_DryWet, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
value=round(
|
||||
int.from_bytes(self._Pitch_Value, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_lo=round(
|
||||
int.from_bytes(self._Pitch_formant_lo, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_med=round(
|
||||
int.from_bytes(self._Pitch_formant_med, 'little', signed=True) * 0.01, 2
|
||||
),
|
||||
formant_high=round(
|
||||
int.from_bytes(self._Pitch_formant_high, 'little', signed=True) * 0.01,
|
||||
2,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketNBS1(VbanRtPacket):
|
||||
"""Represents the body of a VBAN RT data packet with NBS 1"""
|
||||
|
||||
strips: tuple[VbanVMParamStrip, ...]
|
||||
|
||||
@classmethod
|
||||
def from_bytes(
|
||||
cls,
|
||||
nbs: NBS,
|
||||
kind: KindMapClass,
|
||||
data: bytes,
|
||||
):
|
||||
return cls(
|
||||
nbs=nbs,
|
||||
_kind=kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
strips=tuple(
|
||||
VbanVMParamStrip.from_bytes(
|
||||
data[44 + i * VMPARAMSTRIP_SIZE : 44 + (i + 1) * VMPARAMSTRIP_SIZE]
|
||||
)
|
||||
for i in range(16)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class SubscribeHeader:
|
||||
"""Represents the header of an RT subscription packet"""
|
||||
"""Represents the header an RT Packet Service subscription packet"""
|
||||
|
||||
nbs: NBS = NBS.zero
|
||||
name: str = 'Register-RTP'
|
||||
timeout: int = 15
|
||||
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 vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def format_sr(self) -> bytes:
|
||||
return VBAN_PROTOCOL_SERVICE.to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_nbs(self) -> bytes:
|
||||
return (self.nbs.value & 0xFF).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_nbc(self) -> bytes:
|
||||
return VBAN_SERVICE_RTPACKETREGISTER.to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def format_bit(self) -> bytes:
|
||||
return (self.timeout & 0xFF).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
return self.name.encode('ascii') + bytes(16 - len(self.name))
|
||||
|
||||
@classmethod
|
||||
def to_bytes(cls, nbs: NBS, framecounter: int) -> bytes:
|
||||
header = cls(nbs=nbs)
|
||||
|
||||
data = bytearray()
|
||||
data.extend(header.vban)
|
||||
data.extend(header.format_sr)
|
||||
data.extend(header.format_nbs)
|
||||
data.extend(header.format_nbc)
|
||||
data.extend(header.format_bit)
|
||||
data.extend(header.streamname)
|
||||
data.extend(framecounter.to_bytes(4, 'little'))
|
||||
return bytes(data)
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.format_sr
|
||||
header += self.format_nbs
|
||||
header += self.format_nbc
|
||||
header += self.format_bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert len(header) == HEADER_SIZE + 4, (
|
||||
f'expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE + 4} bytes total)'
|
||||
)
|
||||
return header
|
||||
|
||||
|
||||
@dataclass
|
||||
class VbanRtPacketHeader:
|
||||
"""Represents the header of an RT response packet"""
|
||||
"""Represents the header of a VBAN RT response packet"""
|
||||
|
||||
name: str = 'Voicemeeter-RTP'
|
||||
format_sr: int = VBAN_PROTOCOL_SERVICE
|
||||
format_nbs: int = 0
|
||||
format_nbc: int = VBAN_SERVICE_RTPACKET
|
||||
format_bit: int = 0
|
||||
name = 'Voicemeeter-RTP'
|
||||
vban: bytes = 'VBAN'.encode()
|
||||
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, 'little')
|
||||
format_nbs: bytes = (0).to_bytes(1, 'little')
|
||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, 'little')
|
||||
format_bit: bytes = (0).to_bytes(1, 'little')
|
||||
streamname: bytes = name.encode('ascii') + bytes(16 - len(name))
|
||||
|
||||
@property
|
||||
def vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
return self.name.encode('ascii') + bytes(16 - len(self.name))
|
||||
|
||||
@classmethod
|
||||
def from_bytes(cls, data: bytes):
|
||||
if len(data) < HEADER_SIZE:
|
||||
raise ValueError('Data is too short to be a valid VbanRTPPacketHeader')
|
||||
|
||||
name = data[8:24].rstrip(b'\x00').decode('utf-8')
|
||||
return cls(
|
||||
name=name,
|
||||
format_sr=data[4] & VBAN_SERVICE_MASK,
|
||||
format_nbs=data[5],
|
||||
format_nbc=data[6],
|
||||
format_bit=data[7],
|
||||
)
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.format_sr
|
||||
header += self.format_nbs
|
||||
header += self.format_nbc
|
||||
header += self.format_bit
|
||||
header += self.streamname
|
||||
assert len(header) == HEADER_SIZE, f'expected header size {HEADER_SIZE} bytes'
|
||||
return header
|
||||
|
||||
|
||||
@dataclass
|
||||
class RequestHeader:
|
||||
"""Represents the header of an RT request packet"""
|
||||
"""Represents the header of a REQUEST RT PACKET"""
|
||||
|
||||
name: str
|
||||
bps_index: int
|
||||
channel: int
|
||||
framecounter: int = 0
|
||||
vban: bytes = 'VBAN'.encode()
|
||||
nbs: bytes = (0).to_bytes(1, 'little')
|
||||
bit: bytes = (0x10).to_bytes(1, 'little')
|
||||
framecounter: bytes = (0).to_bytes(4, 'little')
|
||||
|
||||
@property
|
||||
def vban(self) -> bytes:
|
||||
return b'VBAN'
|
||||
|
||||
@property
|
||||
def sr(self) -> bytes:
|
||||
def sr(self):
|
||||
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def nbs(self) -> bytes:
|
||||
return (0).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def nbc(self) -> bytes:
|
||||
def nbc(self):
|
||||
return (self.channel).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def bit(self) -> bytes:
|
||||
return (0x10).to_bytes(1, 'little')
|
||||
|
||||
@property
|
||||
def streamname(self) -> bytes:
|
||||
def streamname(self):
|
||||
return self.name.encode() + bytes(16 - len(self.name))
|
||||
|
||||
@classmethod
|
||||
def to_bytes(
|
||||
cls, name: str, bps_index: int, channel: int, framecounter: int
|
||||
) -> bytes:
|
||||
header = cls(
|
||||
name=name, bps_index=bps_index, channel=channel, framecounter=framecounter
|
||||
@property
|
||||
def header(self):
|
||||
header = self.vban
|
||||
header += self.sr
|
||||
header += self.nbs
|
||||
header += self.nbc
|
||||
header += self.bit
|
||||
header += self.streamname
|
||||
header += self.framecounter
|
||||
assert len(header) == HEADER_SIZE + 4, (
|
||||
f'expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE + 4} bytes total)'
|
||||
)
|
||||
|
||||
data = bytearray()
|
||||
data.extend(header.vban)
|
||||
data.extend(header.sr)
|
||||
data.extend(header.nbs)
|
||||
data.extend(header.nbc)
|
||||
data.extend(header.bit)
|
||||
data.extend(header.streamname)
|
||||
data.extend(header.framecounter.to_bytes(4, 'little'))
|
||||
return bytes(data)
|
||||
return header
|
||||
|
||||
@ -1,17 +1,10 @@
|
||||
import abc
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from typing import Union
|
||||
|
||||
from . import kinds
|
||||
from .enums import NBS
|
||||
from .iremote import IRemote
|
||||
from .meta import (
|
||||
channel_bool_prop,
|
||||
channel_label_prop,
|
||||
send_prop,
|
||||
strip_output_prop,
|
||||
xy_prop,
|
||||
)
|
||||
from .kinds import kinds_all
|
||||
from .meta import channel_bool_prop, channel_label_prop, strip_output_prop
|
||||
|
||||
|
||||
class Strip(IRemote):
|
||||
@ -21,7 +14,7 @@ class Strip(IRemote):
|
||||
Defines concrete implementation for strip
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@ -41,7 +34,7 @@ class Strip(IRemote):
|
||||
def gain(self) -> float:
|
||||
val = self.getter('gain')
|
||||
if val is None:
|
||||
val = max(layer.gain for layer in self.gainlayer)
|
||||
val = self.gainlayer[0].gain
|
||||
return round(val, 1)
|
||||
|
||||
@gain.setter
|
||||
@ -59,16 +52,15 @@ class Strip(IRemote):
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, remote, index, is_phys):
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
def make(cls, remote, index):
|
||||
return type(
|
||||
f'PhysicalStrip{remote.kind}',
|
||||
(cls, EFFECTS_cls),
|
||||
(cls,),
|
||||
{
|
||||
'comp': StripComp(remote, index),
|
||||
'gate': StripGate(remote, index),
|
||||
'denoiser': StripDenoiser(remote, index),
|
||||
'eq': StripEQ.make(remote, index),
|
||||
'eq': StripEQ(remote, index),
|
||||
},
|
||||
)
|
||||
|
||||
@ -76,14 +68,12 @@ class PhysicalStrip(Strip):
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
@property
|
||||
def audibility(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.knob
|
||||
def device(self):
|
||||
return
|
||||
|
||||
@audibility.setter
|
||||
def audibility(self, val: float):
|
||||
self.setter('audibility', val)
|
||||
@property
|
||||
def sr(self):
|
||||
return
|
||||
|
||||
|
||||
class StripComp(IRemote):
|
||||
@ -93,9 +83,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.comp
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
@ -103,9 +91,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def gainin(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.gain_in
|
||||
return
|
||||
|
||||
@gainin.setter
|
||||
def gainin(self, val: float):
|
||||
@ -113,9 +99,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def ratio(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.ratio
|
||||
return
|
||||
|
||||
@ratio.setter
|
||||
def ratio(self, val: float):
|
||||
@ -123,9 +107,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.threshold
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
@ -133,9 +115,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.attack_ms
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
@ -143,9 +123,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.release_ms
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
@ -153,9 +131,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def knee(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.n_knee
|
||||
return
|
||||
|
||||
@knee.setter
|
||||
def knee(self, val: float):
|
||||
@ -163,9 +139,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def gainout(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].compressor.gain_out
|
||||
return
|
||||
|
||||
@gainout.setter
|
||||
def gainout(self, val: float):
|
||||
@ -173,9 +147,7 @@ class StripComp(IRemote):
|
||||
|
||||
@property
|
||||
def makeup(self) -> bool:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return False
|
||||
return bool(self.public_packets[NBS.one].strips[self.index].compressor.makeup)
|
||||
return
|
||||
|
||||
@makeup.setter
|
||||
def makeup(self, val: bool):
|
||||
@ -189,9 +161,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.gate
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
@ -199,9 +169,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def threshold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.threshold_in
|
||||
return
|
||||
|
||||
@threshold.setter
|
||||
def threshold(self, val: float):
|
||||
@ -209,9 +177,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def damping(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.damping_max
|
||||
return
|
||||
|
||||
@damping.setter
|
||||
def damping(self, val: float):
|
||||
@ -219,9 +185,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def bpsidechain(self) -> int:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.bp_sidechain
|
||||
return
|
||||
|
||||
@bpsidechain.setter
|
||||
def bpsidechain(self, val: int):
|
||||
@ -229,9 +193,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def attack(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.attack_ms
|
||||
return
|
||||
|
||||
@attack.setter
|
||||
def attack(self, val: float):
|
||||
@ -239,9 +201,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def hold(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.hold_ms
|
||||
return
|
||||
|
||||
@hold.setter
|
||||
def hold(self, val: float):
|
||||
@ -249,9 +209,7 @@ class StripGate(IRemote):
|
||||
|
||||
@property
|
||||
def release(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].gate.release_ms
|
||||
return
|
||||
|
||||
@release.setter
|
||||
def release(self, val: float):
|
||||
@ -265,9 +223,7 @@ class StripDenoiser(IRemote):
|
||||
|
||||
@property
|
||||
def knob(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].audibility.denoiser
|
||||
return
|
||||
|
||||
@knob.setter
|
||||
def knob(self, val: float):
|
||||
@ -275,25 +231,6 @@ class StripDenoiser(IRemote):
|
||||
|
||||
|
||||
class StripEQ(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i):
|
||||
"""
|
||||
Factory method for Strip EQ.
|
||||
|
||||
Returns a StripEQ class.
|
||||
"""
|
||||
STRIPEQ_cls = type(
|
||||
'StripEQ',
|
||||
(cls,),
|
||||
{
|
||||
'channel': tuple(
|
||||
StripEQCh.make(remote, i, j)
|
||||
for j in range(remote.kind.strip_channels)
|
||||
)
|
||||
},
|
||||
)
|
||||
return STRIPEQ_cls(remote, i)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'strip[{self.index}].eq'
|
||||
@ -315,155 +252,7 @@ class StripEQ(IRemote):
|
||||
self.setter('ab', 1 if val else 0)
|
||||
|
||||
|
||||
class StripEQCh(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i, j):
|
||||
"""
|
||||
Factory method for Strip EQ channel.
|
||||
|
||||
Returns a StripEQCh class.
|
||||
"""
|
||||
StripEQCh_cls = type(
|
||||
'StripEQCh',
|
||||
(cls,),
|
||||
{
|
||||
'cell': tuple(
|
||||
StripEQChCell(remote, i, j, k) for k in range(remote.kind.cells)
|
||||
)
|
||||
},
|
||||
)
|
||||
return StripEQCh_cls(remote, i, j)
|
||||
|
||||
def __init__(self, remote, i, j):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Strip[{self.index}].eq.channel[{self.channel_index}]'
|
||||
|
||||
|
||||
class StripEQChCell(IRemote):
|
||||
def __init__(self, remote, i, j, k):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
self.cell_index = k
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Strip[{self.index}].eq.channel[{self.channel_index}].cell[{self.cell_index}]'
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return False
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.on
|
||||
)
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter('on', 1 if val else 0)
|
||||
|
||||
@property
|
||||
def type(self) -> int:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.type
|
||||
)
|
||||
|
||||
@type.setter
|
||||
def type(self, val: int):
|
||||
self.setter('type', val)
|
||||
|
||||
@property
|
||||
def f(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.freq
|
||||
)
|
||||
|
||||
@f.setter
|
||||
def f(self, val: float):
|
||||
self.setter('f', val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.gain
|
||||
)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter('gain', val)
|
||||
|
||||
@property
|
||||
def q(self) -> float:
|
||||
if self.channel_index > 0:
|
||||
self.logger.warning(
|
||||
'Only channel 0 is supported over VBAN for Strip EQ cells'
|
||||
)
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return (
|
||||
self.public_packets[NBS.one]
|
||||
.strips[self.index]
|
||||
.parametric_eq[self.cell_index]
|
||||
.q
|
||||
)
|
||||
|
||||
@q.setter
|
||||
def q(self, val: float):
|
||||
self.setter('q', val)
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, remote, i, is_phys):
|
||||
"""
|
||||
Factory method for VirtualStrip.
|
||||
|
||||
Returns a VirtualStrip class.
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
'VirtualStrip',
|
||||
(cls, EFFECTS_cls),
|
||||
{},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f'{type(self).__name__}{self.index}'
|
||||
|
||||
@ -473,48 +262,12 @@ class VirtualStrip(Strip):
|
||||
|
||||
@property
|
||||
def k(self) -> int:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0
|
||||
return self.public_packets[NBS.one].strips[self.index].karaoke
|
||||
return
|
||||
|
||||
@k.setter
|
||||
def k(self, val: int):
|
||||
self.setter('karaoke', val)
|
||||
|
||||
@property
|
||||
def bass(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.bass
|
||||
|
||||
@bass.setter
|
||||
def bass(self, val: float):
|
||||
self.setter('EQGain1', val)
|
||||
|
||||
@property
|
||||
def mid(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.mid
|
||||
|
||||
@mid.setter
|
||||
def mid(self, val: float):
|
||||
self.setter('EQGain2', val)
|
||||
|
||||
med = mid
|
||||
|
||||
@property
|
||||
def treble(self) -> float:
|
||||
if self.public_packets[NBS.one] is None:
|
||||
return 0.0
|
||||
return self.public_packets[NBS.one].strips[self.index].eqgains.treble
|
||||
|
||||
@treble.setter
|
||||
def treble(self, val: float):
|
||||
self.setter('EQGain3', val)
|
||||
|
||||
high = treble
|
||||
|
||||
def appgain(self, name: str, gain: float):
|
||||
self.setter('AppGain', f'("{name}", {gain})')
|
||||
|
||||
@ -552,7 +305,7 @@ class StripLevel(IRemote):
|
||||
)
|
||||
return tuple(
|
||||
fget(i)
|
||||
for i in self._remote._get_levels(self.public_packets[NBS.zero])[0][
|
||||
for i in self._remote._get_levels(self.public_packet)[0][
|
||||
self.range[0] : self.range[-1]
|
||||
]
|
||||
)
|
||||
@ -596,11 +349,16 @@ class GainLayer(IRemote):
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
def fget():
|
||||
val = getattr(self.public_packet, f'stripgainlayer{self._i + 1}')[
|
||||
self.index
|
||||
]
|
||||
if 0 <= val <= 1200:
|
||||
return val * 0.01
|
||||
return (((1 << 16) - 1) - val) * -0.01
|
||||
|
||||
val = self.getter(f'GainLayer[{self._i}]')
|
||||
if val:
|
||||
return round(val, 2)
|
||||
else:
|
||||
return self.public_packets[NBS.zero].gainlayers[self._i][self.index]
|
||||
return round(val if val else fget(), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
@ -637,64 +395,10 @@ def _make_channelout_mixin(kind):
|
||||
|
||||
|
||||
_make_channelout_mixins = {
|
||||
kind.name: _make_channelout_mixin(kind) for kind in kinds.all
|
||||
kind.name: _make_channelout_mixin(kind) for kind in kinds_all
|
||||
}
|
||||
|
||||
|
||||
def _make_effects_mixin(kind, is_phys):
|
||||
"""creates an effects mixin for a kind"""
|
||||
|
||||
def _make_xy_cls():
|
||||
pan = {param: xy_prop(param) for param in ['pan_x', 'pan_y']}
|
||||
color = {param: xy_prop(param) for param in ['color_x', 'color_y']}
|
||||
fx = {param: xy_prop(param) for param in ['fx_x', 'fx_y']}
|
||||
if is_phys:
|
||||
return type(
|
||||
'XYPhys',
|
||||
(),
|
||||
{
|
||||
**pan,
|
||||
**color,
|
||||
**fx,
|
||||
},
|
||||
)
|
||||
return type(
|
||||
'XYVirt',
|
||||
(),
|
||||
{**pan},
|
||||
)
|
||||
|
||||
def _make_sends_cls():
|
||||
if is_phys:
|
||||
return type(
|
||||
'FX',
|
||||
(),
|
||||
{
|
||||
**{
|
||||
param: send_prop(param)
|
||||
for param in ['reverb', 'delay', 'fx1', 'fx2']
|
||||
},
|
||||
# **{
|
||||
# f'post{param}': bool_prop(f'post{param}')
|
||||
# for param in ['reverb', 'delay', 'fx1', 'fx2']
|
||||
# },
|
||||
},
|
||||
)
|
||||
return type('FX', (), {})
|
||||
|
||||
if kind.name == 'basic':
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == 'banana':
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == 'potato':
|
||||
steps = (_make_xy_cls, _make_sends_cls)
|
||||
return type(f'Effects{kind}', tuple(step() for step in steps), {})
|
||||
|
||||
|
||||
def _make_effects_mixins(is_phys):
|
||||
return {kind.name: _make_effects_mixin(kind, is_phys) for kind in kinds.all}
|
||||
|
||||
|
||||
def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip]:
|
||||
"""
|
||||
Factory method for strips
|
||||
@ -703,11 +407,7 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
||||
|
||||
Returns a physical or virtual strip subclass
|
||||
"""
|
||||
STRIP_cls = (
|
||||
PhysicalStrip.make(remote, i, is_phys_strip)
|
||||
if is_phys_strip
|
||||
else VirtualStrip.make(remote, i, is_phys_strip)
|
||||
)
|
||||
STRIP_cls = PhysicalStrip.make(remote, i) if is_phys_strip else VirtualStrip
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||
|
||||
|
||||
@ -29,20 +29,6 @@ def cache_string(func, param):
|
||||
return wrapper
|
||||
|
||||
|
||||
def cache_float(func, param):
|
||||
"""Check cache for a float prop"""
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
self, *rem = args
|
||||
if self._cmd(param) in self._remote.cache:
|
||||
return round(self._remote.cache.pop(self._cmd(param)), 2)
|
||||
if self._remote.sync:
|
||||
self._remote.clear_dirty()
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def depth(d):
|
||||
if isinstance(d, dict):
|
||||
return 1 + (max(map(depth, d.values())) if d else 0)
|
||||
@ -96,11 +82,3 @@ def deep_merge(dict1, dict2):
|
||||
yield k, dict1[k]
|
||||
else:
|
||||
yield k, dict2[k]
|
||||
|
||||
|
||||
def bump_framecounter(framecounter: int) -> int:
|
||||
"""Increment framecounter with rollover at 0xFFFFFFFF."""
|
||||
if framecounter > 0xFFFFFFFF:
|
||||
return 0
|
||||
else:
|
||||
return framecounter + 1
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import abc
|
||||
from abc import abstractmethod
|
||||
|
||||
from . import kinds
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
|
||||
|
||||
class VbanStream(IRemote):
|
||||
@ -11,7 +11,7 @@ class VbanStream(IRemote):
|
||||
Defines concrete implementation for vban stream
|
||||
"""
|
||||
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
pass
|
||||
|
||||
@ -194,7 +194,7 @@ def _make_stream_pair(remote, kind):
|
||||
|
||||
|
||||
def _make_stream_pairs(remote):
|
||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds.all}
|
||||
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||
|
||||
|
||||
class Vban:
|
||||
|
||||
@ -1,25 +1,24 @@
|
||||
import abc
|
||||
import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from pathlib import Path
|
||||
from queue import Queue
|
||||
from typing import Iterable, Union
|
||||
|
||||
from .enums import NBS
|
||||
from .error import VBANCMDError
|
||||
from .event import Event
|
||||
from .packet import RequestHeader
|
||||
from .subject import Subject
|
||||
from .util import bump_framecounter, deep_merge, script
|
||||
from .util import deep_merge, script
|
||||
from .worker import Producer, Subscriber, Updater
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VbanCmd(abc.ABC):
|
||||
"""Abstract Base Class for Voicemeeter VBAN Command Interfaces"""
|
||||
class VbanCmd(metaclass=ABCMeta):
|
||||
"""Base class responsible for communicating with the VBAN RT Packet Service"""
|
||||
|
||||
DELAY = 0.001
|
||||
# fmt: off
|
||||
@ -38,9 +37,12 @@ class VbanCmd(abc.ABC):
|
||||
for attr, val in kwargs.items():
|
||||
setattr(self, attr, val)
|
||||
|
||||
self._framecounter = 0
|
||||
self.packet_request = RequestHeader(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
)
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.subject = self.observer = Subject()
|
||||
self.cache = {}
|
||||
self._pdirty = False
|
||||
@ -49,7 +51,7 @@ class VbanCmd(abc.ABC):
|
||||
self.stop_event = None
|
||||
self.producer = None
|
||||
|
||||
@abc.abstractmethod
|
||||
@abstractmethod
|
||||
def __str__(self):
|
||||
"""Ensure subclasses override str magic method"""
|
||||
pass
|
||||
@ -58,7 +60,7 @@ class VbanCmd(abc.ABC):
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib # type: ignore[import]
|
||||
import tomli as tomllib
|
||||
|
||||
def get_filepath():
|
||||
for pn in (
|
||||
@ -122,49 +124,37 @@ class VbanCmd(abc.ABC):
|
||||
|
||||
def _set_rt(self, cmd: str, val: Union[str, float]):
|
||||
"""Sends a string request command over a network."""
|
||||
req_packet = RequestHeader.to_bytes(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
framecounter=self._framecounter,
|
||||
)
|
||||
self.sock.sendto(
|
||||
req_packet + f'{cmd}={val};'.encode(),
|
||||
self.packet_request.header + f'{cmd}={val};'.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, 'little') + 1
|
||||
).to_bytes(4, 'little')
|
||||
self.cache[cmd] = val
|
||||
|
||||
@script
|
||||
def sendtext(self, script):
|
||||
"""Sends a multiple parameter string over a network."""
|
||||
req_packet = RequestHeader.to_bytes(
|
||||
name=self.streamname,
|
||||
bps_index=self.BPS_OPTS.index(self.bps),
|
||||
channel=self.channel,
|
||||
framecounter=self._framecounter,
|
||||
)
|
||||
self.sock.sendto(
|
||||
req_packet + script.encode(),
|
||||
self.packet_request.header + script.encode(),
|
||||
(socket.gethostbyname(self.ip), self.port),
|
||||
)
|
||||
self._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
self.packet_request.framecounter = (
|
||||
int.from_bytes(self.packet_request.framecounter, 'little') + 1
|
||||
).to_bytes(4, 'little')
|
||||
self.logger.debug(f'sendtext: {script}')
|
||||
time.sleep(self.DELAY)
|
||||
|
||||
@property
|
||||
def type(self) -> str:
|
||||
"""Returns the type of Voicemeeter installation."""
|
||||
return self.public_packets[NBS.zero].voicemeetertype
|
||||
return self.public_packet.voicemeetertype
|
||||
|
||||
@property
|
||||
def version(self) -> str:
|
||||
"""Returns Voicemeeter's version as a string"""
|
||||
return '{0}.{1}.{2}.{3}'.format(
|
||||
*self.public_packets[NBS.zero].voicemeeterversion
|
||||
)
|
||||
return '{0}.{1}.{2}.{3}'.format(*self.public_packet.voicemeeterversion)
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
@ -177,8 +167,8 @@ class VbanCmd(abc.ABC):
|
||||
return self._ldirty
|
||||
|
||||
@property
|
||||
def public_packets(self):
|
||||
return self._public_packets
|
||||
def public_packet(self):
|
||||
return self._public_packet
|
||||
|
||||
def clear_dirty(self) -> None:
|
||||
while self.pdirty:
|
||||
|
||||
@ -2,20 +2,10 @@ import logging
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .enums import NBS
|
||||
from .error import VBANCMDConnectionError
|
||||
from .packet import (
|
||||
HEADER_SIZE,
|
||||
VBAN_PROTOCOL_SERVICE,
|
||||
VBAN_SERVICE_RTPACKET,
|
||||
SubscribeHeader,
|
||||
VbanRtPacket,
|
||||
VbanRtPacketHeader,
|
||||
VbanRtPacketNBS0,
|
||||
VbanRtPacketNBS1,
|
||||
)
|
||||
from .util import bump_framecounter
|
||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -28,18 +18,18 @@ class Subscriber(threading.Thread):
|
||||
self._remote = remote
|
||||
self.stop_event = stop_event
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self._framecounter = 0
|
||||
self.packet = SubscribeHeader()
|
||||
|
||||
def run(self):
|
||||
while not self.stopped():
|
||||
try:
|
||||
for nbs in NBS:
|
||||
sub_packet = SubscribeHeader().to_bytes(nbs, self._framecounter)
|
||||
self._remote.sock.sendto(
|
||||
sub_packet, (self._remote.ip, self._remote.port)
|
||||
self.packet.header,
|
||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
||||
)
|
||||
self._framecounter = bump_framecounter(self._framecounter)
|
||||
|
||||
self.packet.framecounter = (
|
||||
int.from_bytes(self.packet.framecounter, 'little') + 1
|
||||
).to_bytes(4, 'little')
|
||||
self.wait_until_stopped(10)
|
||||
except socket.gaierror as e:
|
||||
self.logger.exception(f'{type(e).__name__}: {e}')
|
||||
@ -68,46 +58,57 @@ class Producer(threading.Thread):
|
||||
self.queue = queue
|
||||
self.stop_event = stop_event
|
||||
self.logger = logger.getChild(self.__class__.__name__)
|
||||
self.packet_expected = VbanRtPacketHeader()
|
||||
self._remote.sock.settimeout(self._remote.timeout)
|
||||
self._remote._public_packets = [None] * (max(NBS) + 1)
|
||||
_pp = self._get_rt()
|
||||
self._remote._public_packets[_pp.nbs] = _pp
|
||||
self._remote._public_packet = self._get_rt()
|
||||
(
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
) = self._remote._get_levels(self._remote.public_packets[NBS.zero])
|
||||
) = self._remote._get_levels(self._remote.public_packet)
|
||||
|
||||
def _get_rt(self) -> VbanRtPacket:
|
||||
"""Attempt to fetch data packet until a valid one found"""
|
||||
|
||||
while True:
|
||||
if resp := self._fetch_rt_packet():
|
||||
return resp
|
||||
def fget():
|
||||
data = None
|
||||
while not data:
|
||||
data = self._fetch_rt_packet()
|
||||
return data
|
||||
|
||||
def _fetch_rt_packet(self) -> VbanRtPacket | None:
|
||||
return fget()
|
||||
|
||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||
try:
|
||||
data, _ = self._remote.sock.recvfrom(2048)
|
||||
if len(data) < HEADER_SIZE:
|
||||
return
|
||||
|
||||
response_header = VbanRtPacketHeader.from_bytes(data[:HEADER_SIZE])
|
||||
if (
|
||||
response_header.format_sr != VBAN_PROTOCOL_SERVICE
|
||||
or response_header.format_nbc != VBAN_SERVICE_RTPACKET
|
||||
):
|
||||
return
|
||||
|
||||
match response_header.format_nbs:
|
||||
case NBS.zero:
|
||||
return VbanRtPacketNBS0.from_bytes(
|
||||
nbs=NBS.zero, kind=self._remote.kind, data=data
|
||||
# do we have packet data?
|
||||
if len(data) > HEADER_SIZE:
|
||||
# is the packet of type VBAN RT response?
|
||||
if self.packet_expected.header == data[:HEADER_SIZE]:
|
||||
return VbanRtPacket(
|
||||
_kind=self._remote.kind,
|
||||
_voicemeeterType=data[28:29],
|
||||
_reserved=data[29:30],
|
||||
_buffersize=data[30:32],
|
||||
_voicemeeterVersion=data[32:36],
|
||||
_optionBits=data[36:40],
|
||||
_samplerate=data[40:44],
|
||||
_inputLeveldB100=data[44:112],
|
||||
_outputLeveldB100=data[112:240],
|
||||
_TransportBit=data[240:244],
|
||||
_stripState=data[244:276],
|
||||
_busState=data[276:308],
|
||||
_stripGaindB100Layer1=data[308:324],
|
||||
_stripGaindB100Layer2=data[324:340],
|
||||
_stripGaindB100Layer3=data[340:356],
|
||||
_stripGaindB100Layer4=data[356:372],
|
||||
_stripGaindB100Layer5=data[372:388],
|
||||
_stripGaindB100Layer6=data[388:404],
|
||||
_stripGaindB100Layer7=data[404:420],
|
||||
_stripGaindB100Layer8=data[420:436],
|
||||
_busGaindB100=data[436:452],
|
||||
_stripLabelUTF8c60=data[452:932],
|
||||
_busLabelUTF8c60=data[932:1412],
|
||||
)
|
||||
|
||||
case NBS.one:
|
||||
return VbanRtPacketNBS1.from_bytes(
|
||||
nbs=NBS.one, kind=self._remote.kind, data=data
|
||||
)
|
||||
return None
|
||||
except TimeoutError as e:
|
||||
self.logger.exception(f'{type(e).__name__}: {e}')
|
||||
raise VBANCMDConnectionError(
|
||||
@ -119,20 +120,14 @@ class Producer(threading.Thread):
|
||||
|
||||
def run(self):
|
||||
while not self.stopped():
|
||||
pdirty = ldirty = False
|
||||
_pp = self._get_rt()
|
||||
match _pp.nbs:
|
||||
case NBS.zero:
|
||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||
ldirty = _pp.ldirty(
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
self._remote.cache['strip_level'], self._remote.cache['bus_level']
|
||||
)
|
||||
pdirty = _pp.pdirty(self._remote.public_packets[NBS.zero])
|
||||
case NBS.one:
|
||||
pdirty = True
|
||||
|
||||
if pdirty or ldirty:
|
||||
self._remote._public_packets[_pp.nbs] = _pp
|
||||
self._remote._public_packet = _pp
|
||||
self._remote._pdirty = pdirty
|
||||
self._remote._ldirty = ldirty
|
||||
|
||||
@ -171,15 +166,15 @@ class Updater(threading.Thread):
|
||||
self._remote.subject.notify(event)
|
||||
elif event == 'ldirty' and self._remote.ldirty:
|
||||
self._remote._strip_comp, self._remote._bus_comp = (
|
||||
self._remote._public_packets[NBS.zero]._strip_comp,
|
||||
self._remote._public_packets[NBS.zero]._bus_comp,
|
||||
self._remote._public_packet._strip_comp,
|
||||
self._remote._public_packet._bus_comp,
|
||||
)
|
||||
(
|
||||
self._remote.cache['strip_level'],
|
||||
self._remote.cache['bus_level'],
|
||||
) = (
|
||||
self._remote._public_packets[NBS.zero].inputlevels,
|
||||
self._remote._public_packets[NBS.zero].outputlevels,
|
||||
self._remote._public_packet.inputlevels,
|
||||
self._remote._public_packet.outputlevels,
|
||||
)
|
||||
self._remote.subject.notify(event)
|
||||
self.logger.debug(f'terminating {self.name} thread')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user