10 Commits

Author SHA1 Message Date
a8ef82166c upd publish action 2026-02-27 20:59:25 +00:00
79f06ecc79 add ruff+publish workflows 2026-02-27 20:57:54 +00:00
b291c3a477 minor version bump 2026-02-27 20:36:54 +00:00
c335d35b9f fix config extends section 2026-02-27 20:16:04 +00:00
911d2f64a6 import abc namespace 2026-02-08 09:09:59 +00:00
e58d6c7242 remove comments 2026-01-18 19:57:12 +00:00
870a95b41e upd Strip Comp/Gate/EQ in README 2026-01-18 18:08:40 +00:00
59880bf582 remove comments 2026-01-18 17:22:20 +00:00
cc58d1f081 implement {strip}.gate 2026-01-18 17:06:10 +00:00
e37dea38b3 upd Run Tests in README 2026-01-18 15:25:05 +00:00
12 changed files with 230 additions and 116 deletions

53
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,53 @@
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 Normal file
View File

@@ -0,0 +1,19 @@
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'

View File

@@ -11,6 +11,13 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [x] - [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 ## [2.5.2] - 2025-01-25
### Changed ### Changed

View File

@@ -171,9 +171,7 @@ example:
print(vban.strip[4].comp.knob) print(vban.strip[4].comp.knob)
``` ```
Strip Comp properties are defined as write only. Strip Comp `knob` is defined for all versions, all other parameters potato only.
`knob` defined for all versions, all other parameters potato only.
##### Strip.Gate ##### Strip.Gate
@@ -193,9 +191,7 @@ example:
vban.strip[2].gate.attack = 300.8 vban.strip[2].gate.attack = 300.8
``` ```
Strip Gate properties are defined as write only, potato version only. Strip Gate `knob` is defined for all versions, all other parameters potato only.
`knob` defined for all versions, all other parameters potato only.
##### Strip.Denoiser ##### Strip.Denoiser
@@ -212,7 +208,32 @@ The following properties are available.
- `on`: boolean - `on`: boolean
- `ab`: boolean - `ab`: boolean
Strip EQ properties are defined as write only, potato version only. 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.
##### Gainlayers ##### Gainlayers
@@ -400,8 +421,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: Three example 'extender' configs are included with the repo. You may load them with:
```python ```python
import voicemeeterlib import vban_cmd
with voicemeeterlib.api('banana') as vm: with vban_cmd.api('banana') as vm:
vm.apply_config('extender') vm.apply_config('extender')
``` ```
@@ -528,13 +549,15 @@ with vban_cmd.api('banana', **opts) as vban:
... ...
``` ```
## Tests ### Run tests
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation) Install [poetry](https://python-poetry.org/docs/#installation) and then:
Then from tests directory: ```powershell
poetry poe test-basic
`pytest -v` poetry poe test-banana
poetry poe test-potato
```
## Resources ## Resources

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "vban-cmd" name = "vban-cmd"
version = "2.5.2" version = "2.6.0"
description = "Python interface for the VBAN RT Packet Service (Sendtext)" description = "Python interface for the VBAN RT Packet Service (Sendtext)"
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }] authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
license = { text = "MIT" } license = { text = "MIT" }

View File

@@ -1,5 +1,5 @@
import abc
import time import time
from abc import abstractmethod
from typing import Union from typing import Union
from .enums import NBS, BusModes from .enums import NBS, BusModes
@@ -14,7 +14,7 @@ class Bus(IRemote):
Defines concrete implementation for bus Defines concrete implementation for bus
""" """
@abstractmethod @abc.abstractmethod
def __str__(self): def __str__(self):
pass pass

View File

@@ -1,5 +1,5 @@
import abc
import logging import logging
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 from typing import Iterable
@@ -115,7 +115,7 @@ class FactoryBase(VbanCmd):
) )
@property @property
@abstractmethod @abc.abstractmethod
def steps(self): def steps(self):
pass pass

View File

@@ -1,6 +1,6 @@
import abc
import logging import logging
import time import time
from abc import ABCMeta, abstractmethod
from dataclasses import dataclass from dataclasses import dataclass
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -78,7 +78,7 @@ class Modes:
) )
class IRemote(metaclass=ABCMeta): class IRemote(abc.ABC):
""" """
Common interface between base class and extended (higher) classes Common interface between base class and extended (higher) classes
@@ -111,7 +111,7 @@ class IRemote(metaclass=ABCMeta):
return ''.join(cmd) return ''.join(cmd)
@property @property
@abstractmethod @abc.abstractmethod
def identifier(self): def identifier(self):
pass pass

View File

@@ -24,34 +24,34 @@ class VbanRtPacket:
nbs: NBS nbs: NBS
_kind: KindMapClass _kind: KindMapClass
_voicemeeterType: bytes # data[28:29] _voicemeeterType: bytes
_reserved: bytes # data[29:30] _reserved: bytes
_buffersize: bytes # data[30:32] _buffersize: bytes
_voicemeeterVersion: bytes # data[32:36] _voicemeeterVersion: bytes
_optionBits: bytes # data[36:40] _optionBits: bytes
_samplerate: bytes # data[40:44] _samplerate: bytes
@dataclass @dataclass
class VbanRtPacketNBS0(VbanRtPacket): class VbanRtPacketNBS0(VbanRtPacket):
"""Represents the body of a VBAN RT data packet with NBS 0""" """Represents the body of a VBAN RT data packet with NBS 0"""
_inputLeveldB100: bytes # data[44:112] _inputLeveldB100: bytes
_outputLeveldB100: bytes # data[112:240] _outputLeveldB100: bytes
_TransportBit: bytes # data[240:244] _TransportBit: bytes
_stripState: bytes # data[244:276] _stripState: bytes
_busState: bytes # data[276:308] _busState: bytes
_stripGaindB100Layer1: bytes # data[308:324] _stripGaindB100Layer1: bytes
_stripGaindB100Layer2: bytes # data[324:340] _stripGaindB100Layer2: bytes
_stripGaindB100Layer3: bytes # data[340:356] _stripGaindB100Layer3: bytes
_stripGaindB100Layer4: bytes # data[356:372] _stripGaindB100Layer4: bytes
_stripGaindB100Layer5: bytes # data[372:388] _stripGaindB100Layer5: bytes
_stripGaindB100Layer6: bytes # data[388:404] _stripGaindB100Layer6: bytes
_stripGaindB100Layer7: bytes # data[404:420] _stripGaindB100Layer7: bytes
_stripGaindB100Layer8: bytes # data[420:436] _stripGaindB100Layer8: bytes
_busGaindB100: bytes # data[436:452] _busGaindB100: bytes
_stripLabelUTF8c60: bytes # data[452:932] _stripLabelUTF8c60: bytes
_busLabelUTF8c60: bytes # data[932:1412] _busLabelUTF8c60: bytes
@classmethod @classmethod
def from_bytes(cls, nbs: NBS, kind: KindMapClass, data: bytes): def from_bytes(cls, nbs: NBS, kind: KindMapClass, data: bytes):
@@ -260,7 +260,7 @@ class CompressorSettings(NamedTuple):
attack_ms: float attack_ms: float
release_ms: float release_ms: float
n_knee: float n_knee: float
comprate: float ratio: float
threshold: float threshold: float
c_enabled: bool c_enabled: bool
makeup: bool makeup: bool
@@ -268,9 +268,9 @@ class CompressorSettings(NamedTuple):
class GateSettings(NamedTuple): class GateSettings(NamedTuple):
dBThreshold_in: float threshold_in: float
dBDamping_max: float damping_max: float
BP_Sidechain: bool bp_sidechain: bool
attack_ms: float attack_ms: float
hold_ms: float hold_ms: float
release_ms: float release_ms: float
@@ -293,60 +293,60 @@ class PitchSettings(NamedTuple):
class VbanVMParamStrip: class VbanVMParamStrip:
"""Represents the VBAN_VMPARAMSTRIP_PACKET structure""" """Represents the VBAN_VMPARAMSTRIP_PACKET structure"""
_mode: bytes # long = 4 bytes data[0:4] _mode: bytes
_dblevel: bytes # float = 4 bytes data[4:8] _dblevel: bytes
_audibility: bytes # short = 2 bytes data[8:10] _audibility: bytes
_pos3D_x: bytes # short = 2 bytes data[10:12] _pos3D_x: bytes
_pos3D_y: bytes # short = 2 bytes data[12:14] _pos3D_y: bytes
_posColor_x: bytes # short = 2 bytes data[14:16] _posColor_x: bytes
_posColor_y: bytes # short = 2 bytes data[16:18] _posColor_y: bytes
_EQgain1: bytes # short = 2 bytes data[18:20] _EQgain1: bytes
_EQgain2: bytes # short = 2 bytes data[20:22] _EQgain2: bytes
_EQgain3: bytes # short = 2 bytes data[22:24] _EQgain3: bytes
# First channel parametric EQ # First channel parametric EQ
_PEQ_eqOn: bytes # 6 * char = 6 bytes data[24:30] _PEQ_eqOn: bytes
_PEQ_eqtype: bytes # 6 * char = 6 bytes data[30:36] _PEQ_eqtype: bytes
_PEQ_eqgain: bytes # 6 * float = 24 bytes data[36:60] _PEQ_eqgain: bytes
_PEQ_eqfreq: bytes # 6 * float = 24 bytes data[60:84] _PEQ_eqfreq: bytes
_PEQ_eqq: bytes # 6 * float = 24 bytes data[84:108] _PEQ_eqq: bytes
_audibility_c: bytes # short = 2 bytes data[108:110] _audibility_c: bytes
_audibility_g: bytes # short = 2 bytes data[110:112] _audibility_g: bytes
_audibility_d: bytes # short = 2 bytes data[112:114] _audibility_d: bytes
_posMod_x: bytes # short = 2 bytes data[114:116] _posMod_x: bytes
_posMod_y: bytes # short = 2 bytes data[116:118] _posMod_y: bytes
_send_reverb: bytes # short = 2 bytes data[118:120] _send_reverb: bytes
_send_delay: bytes # short = 2 bytes data[120:122] _send_delay: bytes
_send_fx1: bytes # short = 2 bytes data[122:124] _send_fx1: bytes
_send_fx2: bytes # short = 2 bytes data[124:126] _send_fx2: bytes
_dblimit: bytes # short = 2 bytes data[126:128] _dblimit: bytes
_nKaraoke: bytes # short = 2 bytes data[128:130] _nKaraoke: bytes
_COMP_gain_in: bytes # short = 2 bytes data[130:132] _COMP_gain_in: bytes
_COMP_attack_ms: bytes # short = 2 bytes data[132:134] _COMP_attack_ms: bytes
_COMP_release_ms: bytes # short = 2 bytes data[134:136] _COMP_release_ms: bytes
_COMP_n_knee: bytes # short = 2 bytes data[136:138] _COMP_n_knee: bytes
_COMP_comprate: bytes # short = 2 bytes data[138:140] _COMP_comprate: bytes
_COMP_threshold: bytes # short = 2 bytes data[140:142] _COMP_threshold: bytes
_COMP_c_enabled: bytes # short = 2 bytes data[142:144] _COMP_c_enabled: bytes
_COMP_c_auto: bytes # short = 2 bytes data[144:146] _COMP_c_auto: bytes
_COMP_gain_out: bytes # short = 2 bytes data[146:148] _COMP_gain_out: bytes
_GATE_dBThreshold_in: bytes # short = 2 bytes data[148:150] _GATE_dBThreshold_in: bytes
_GATE_dBDamping_max: bytes # short = 2 bytes data[150:152] _GATE_dBDamping_max: bytes
_GATE_BP_Sidechain: bytes # short = 2 bytes data[152:154] _GATE_BP_Sidechain: bytes
_GATE_attack_ms: bytes # short = 2 bytes data[154:156] _GATE_attack_ms: bytes
_GATE_hold_ms: bytes # short = 2 bytes data[156:158] _GATE_hold_ms: bytes
_GATE_release_ms: bytes # short = 2 bytes data[158:160] _GATE_release_ms: bytes
_DenoiserThreshold: bytes # short = 2 bytes data[160:162] _DenoiserThreshold: bytes
_PitchEnabled: bytes # short = 2 bytes data[162:164] _PitchEnabled: bytes
_Pitch_DryWet: bytes # short = 2 bytes data[164:166] _Pitch_DryWet: bytes
_Pitch_Value: bytes # short = 2 bytes data[166:168] _Pitch_Value: bytes
_Pitch_formant_lo: bytes # short = 2 bytes data[168:170] _Pitch_formant_lo: bytes
_Pitch_formant_med: bytes # short = 2 bytes data[170:172] _Pitch_formant_med: bytes
_Pitch_formant_high: bytes # short = 2 bytes data[172:174] _Pitch_formant_high: bytes
@classmethod @classmethod
def from_bytes(cls, data: bytes): def from_bytes(cls, data: bytes):
@@ -473,7 +473,7 @@ class VbanVMParamStrip:
attack_ms=round(int.from_bytes(self._COMP_attack_ms, 'little') * 0.1, 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), 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), n_knee=round(int.from_bytes(self._COMP_n_knee, 'little') * 0.01, 2),
comprate=round(int.from_bytes(self._COMP_comprate, 'little') * 0.01, 2), ratio=round(int.from_bytes(self._COMP_comprate, 'little') * 0.01, 2),
threshold=round( threshold=round(
int.from_bytes(self._COMP_threshold, 'little', signed=True) * 0.01, 2 int.from_bytes(self._COMP_threshold, 'little', signed=True) * 0.01, 2
), ),
@@ -487,15 +487,15 @@ class VbanVMParamStrip:
@property @property
def gate(self) -> GateSettings: def gate(self) -> GateSettings:
return GateSettings( return GateSettings(
dBThreshold_in=round( threshold_in=round(
int.from_bytes(self._GATE_dBThreshold_in, 'little', signed=True) * 0.01, int.from_bytes(self._GATE_dBThreshold_in, 'little', signed=True) * 0.01,
2, 2,
), ),
dBDamping_max=round( damping_max=round(
int.from_bytes(self._GATE_dBDamping_max, 'little', signed=True) * 0.01, int.from_bytes(self._GATE_dBDamping_max, 'little', signed=True) * 0.01,
2, 2,
), ),
BP_Sidechain=round( bp_sidechain=round(
int.from_bytes(self._GATE_BP_Sidechain, 'little') * 0.1, 2 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), attack_ms=round(int.from_bytes(self._GATE_attack_ms, 'little') * 0.1, 2),

View File

@@ -1,5 +1,5 @@
import abc
import time import time
from abc import abstractmethod
from typing import Union from typing import Union
from . import kinds from . import kinds
@@ -21,7 +21,7 @@ class Strip(IRemote):
Defines concrete implementation for strip Defines concrete implementation for strip
""" """
@abstractmethod @abc.abstractmethod
def __str__(self): def __str__(self):
pass pass
@@ -115,7 +115,7 @@ class StripComp(IRemote):
def ratio(self) -> float: def ratio(self) -> float:
if self.public_packets[NBS.one] is None: if self.public_packets[NBS.one] is None:
return 0.0 return 0.0
return self.public_packets[NBS.one].strips[self.index].compressor.comprate return self.public_packets[NBS.one].strips[self.index].compressor.ratio
@ratio.setter @ratio.setter
def ratio(self, val: float): def ratio(self, val: float):
@@ -199,7 +199,9 @@ class StripGate(IRemote):
@property @property
def threshold(self) -> float: def threshold(self) -> float:
return if self.public_packets[NBS.one] is None:
return 0.0
return self.public_packets[NBS.one].strips[self.index].gate.threshold_in
@threshold.setter @threshold.setter
def threshold(self, val: float): def threshold(self, val: float):
@@ -207,7 +209,9 @@ class StripGate(IRemote):
@property @property
def damping(self) -> float: def damping(self) -> float:
return if self.public_packets[NBS.one] is None:
return 0.0
return self.public_packets[NBS.one].strips[self.index].gate.damping_max
@damping.setter @damping.setter
def damping(self, val: float): def damping(self, val: float):
@@ -215,7 +219,9 @@ class StripGate(IRemote):
@property @property
def bpsidechain(self) -> int: def bpsidechain(self) -> int:
return if self.public_packets[NBS.one] is None:
return 0
return self.public_packets[NBS.one].strips[self.index].gate.bp_sidechain
@bpsidechain.setter @bpsidechain.setter
def bpsidechain(self, val: int): def bpsidechain(self, val: int):
@@ -223,7 +229,9 @@ class StripGate(IRemote):
@property @property
def attack(self) -> float: def attack(self) -> float:
return if self.public_packets[NBS.one] is None:
return 0.0
return self.public_packets[NBS.one].strips[self.index].gate.attack_ms
@attack.setter @attack.setter
def attack(self, val: float): def attack(self, val: float):
@@ -231,7 +239,9 @@ class StripGate(IRemote):
@property @property
def hold(self) -> float: def hold(self) -> float:
return if self.public_packets[NBS.one] is None:
return 0.0
return self.public_packets[NBS.one].strips[self.index].gate.hold_ms
@hold.setter @hold.setter
def hold(self, val: float): def hold(self, val: float):
@@ -239,7 +249,9 @@ class StripGate(IRemote):
@property @property
def release(self) -> float: def release(self) -> float:
return if self.public_packets[NBS.one] is None:
return 0.0
return self.public_packets[NBS.one].strips[self.index].gate.release_ms
@release.setter @release.setter
def release(self, val: float): def release(self, val: float):

View File

@@ -1,4 +1,4 @@
from abc import abstractmethod import abc
from . import kinds from . import kinds
from .iremote import IRemote from .iremote import IRemote
@@ -11,7 +11,7 @@ class VbanStream(IRemote):
Defines concrete implementation for vban stream Defines concrete implementation for vban stream
""" """
@abstractmethod @abc.abstractmethod
def __str__(self): def __str__(self):
pass pass

View File

@@ -1,8 +1,8 @@
import abc
import logging import logging
import socket import socket
import threading import threading
import time import time
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, Union from typing import Iterable, Union
@@ -18,8 +18,8 @@ from .worker import Producer, Subscriber, Updater
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class VbanCmd(metaclass=ABCMeta): class VbanCmd(abc.ABC):
"""Base class responsible for communicating with the VBAN RT Packet Service""" """Abstract Base Class for Voicemeeter VBAN Command Interfaces"""
DELAY = 0.001 DELAY = 0.001
# fmt: off # fmt: off
@@ -49,7 +49,7 @@ class VbanCmd(metaclass=ABCMeta):
self.stop_event = None self.stop_event = None
self.producer = None self.producer = None
@abstractmethod @abc.abstractmethod
def __str__(self): def __str__(self):
"""Ensure subclasses override str magic method""" """Ensure subclasses override str magic method"""
pass pass
@@ -58,7 +58,7 @@ class VbanCmd(metaclass=ABCMeta):
try: try:
import tomllib import tomllib
except ModuleNotFoundError: except ModuleNotFoundError:
import tomli as tomllib import tomli as tomllib # type: ignore[import]
def get_filepath(): def get_filepath():
for pn in ( for pn in (