8 Commits

Author SHA1 Message Date
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
10 changed files with 158 additions and 116 deletions

View File

@@ -11,6 +11,13 @@ 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

View File

@@ -171,9 +171,7 @@ example:
print(vban.strip[4].comp.knob)
```
Strip Comp properties are defined as write only.
`knob` defined for all versions, all other parameters potato only.
Strip Comp `knob` is defined for all versions, all other parameters potato only.
##### Strip.Gate
@@ -193,9 +191,7 @@ example:
vban.strip[2].gate.attack = 300.8
```
Strip Gate properties are defined as write only, potato version only.
`knob` defined for all versions, all other parameters potato only.
Strip Gate `knob` is defined for all versions, all other parameters potato only.
##### Strip.Denoiser
@@ -212,7 +208,32 @@ The following properties are available.
- `on`: 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
@@ -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:
```python
import voicemeeterlib
with voicemeeterlib.api('banana') as vm:
import vban_cmd
with vban_cmd.api('banana') as vm:
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:
`pytest -v`
```powershell
poetry poe test-basic
poetry poe test-banana
poetry poe test-potato
```
## Resources

View File

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

View File

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

View File

@@ -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
@abstractmethod
@abc.abstractmethod
def steps(self):
pass

View File

@@ -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(metaclass=ABCMeta):
class IRemote(abc.ABC):
"""
Common interface between base class and extended (higher) classes
@@ -111,7 +111,7 @@ class IRemote(metaclass=ABCMeta):
return ''.join(cmd)
@property
@abstractmethod
@abc.abstractmethod
def identifier(self):
pass

View File

@@ -24,34 +24,34 @@ class VbanRtPacket:
nbs: NBS
_kind: KindMapClass
_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]
_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 # 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]
_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):
@@ -260,7 +260,7 @@ class CompressorSettings(NamedTuple):
attack_ms: float
release_ms: float
n_knee: float
comprate: float
ratio: float
threshold: float
c_enabled: bool
makeup: bool
@@ -268,9 +268,9 @@ class CompressorSettings(NamedTuple):
class GateSettings(NamedTuple):
dBThreshold_in: float
dBDamping_max: float
BP_Sidechain: bool
threshold_in: float
damping_max: float
bp_sidechain: bool
attack_ms: float
hold_ms: float
release_ms: float
@@ -293,60 +293,60 @@ class PitchSettings(NamedTuple):
class VbanVMParamStrip:
"""Represents the VBAN_VMPARAMSTRIP_PACKET structure"""
_mode: bytes # long = 4 bytes data[0:4]
_dblevel: bytes # float = 4 bytes data[4:8]
_audibility: bytes # short = 2 bytes data[8:10]
_pos3D_x: bytes # short = 2 bytes data[10:12]
_pos3D_y: bytes # short = 2 bytes data[12:14]
_posColor_x: bytes # short = 2 bytes data[14:16]
_posColor_y: bytes # short = 2 bytes data[16:18]
_EQgain1: bytes # short = 2 bytes data[18:20]
_EQgain2: bytes # short = 2 bytes data[20:22]
_EQgain3: bytes # short = 2 bytes data[22:24]
_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 # 6 * char = 6 bytes data[24:30]
_PEQ_eqtype: bytes # 6 * char = 6 bytes data[30:36]
_PEQ_eqgain: bytes # 6 * float = 24 bytes data[36:60]
_PEQ_eqfreq: bytes # 6 * float = 24 bytes data[60:84]
_PEQ_eqq: bytes # 6 * float = 24 bytes data[84:108]
_PEQ_eqOn: bytes
_PEQ_eqtype: bytes
_PEQ_eqgain: bytes
_PEQ_eqfreq: bytes
_PEQ_eqq: bytes
_audibility_c: bytes # short = 2 bytes data[108:110]
_audibility_g: bytes # short = 2 bytes data[110:112]
_audibility_d: bytes # short = 2 bytes data[112:114]
_posMod_x: bytes # short = 2 bytes data[114:116]
_posMod_y: bytes # short = 2 bytes data[116:118]
_send_reverb: bytes # short = 2 bytes data[118:120]
_send_delay: bytes # short = 2 bytes data[120:122]
_send_fx1: bytes # short = 2 bytes data[122:124]
_send_fx2: bytes # short = 2 bytes data[124:126]
_dblimit: bytes # short = 2 bytes data[126:128]
_nKaraoke: bytes # short = 2 bytes data[128:130]
_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 # short = 2 bytes data[130:132]
_COMP_attack_ms: bytes # short = 2 bytes data[132:134]
_COMP_release_ms: bytes # short = 2 bytes data[134:136]
_COMP_n_knee: bytes # short = 2 bytes data[136:138]
_COMP_comprate: bytes # short = 2 bytes data[138:140]
_COMP_threshold: bytes # short = 2 bytes data[140:142]
_COMP_c_enabled: bytes # short = 2 bytes data[142:144]
_COMP_c_auto: bytes # short = 2 bytes data[144:146]
_COMP_gain_out: bytes # short = 2 bytes data[146:148]
_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 # short = 2 bytes data[148:150]
_GATE_dBDamping_max: bytes # short = 2 bytes data[150:152]
_GATE_BP_Sidechain: bytes # short = 2 bytes data[152:154]
_GATE_attack_ms: bytes # short = 2 bytes data[154:156]
_GATE_hold_ms: bytes # short = 2 bytes data[156:158]
_GATE_release_ms: bytes # short = 2 bytes data[158:160]
_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 # short = 2 bytes data[160:162]
_PitchEnabled: bytes # short = 2 bytes data[162:164]
_Pitch_DryWet: bytes # short = 2 bytes data[164:166]
_Pitch_Value: bytes # short = 2 bytes data[166:168]
_Pitch_formant_lo: bytes # short = 2 bytes data[168:170]
_Pitch_formant_med: bytes # short = 2 bytes data[170:172]
_Pitch_formant_high: bytes # short = 2 bytes data[172:174]
_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):
@@ -473,7 +473,7 @@ class VbanVMParamStrip:
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),
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(
int.from_bytes(self._COMP_threshold, 'little', signed=True) * 0.01, 2
),
@@ -487,15 +487,15 @@ class VbanVMParamStrip:
@property
def gate(self) -> GateSettings:
return GateSettings(
dBThreshold_in=round(
threshold_in=round(
int.from_bytes(self._GATE_dBThreshold_in, 'little', signed=True) * 0.01,
2,
),
dBDamping_max=round(
damping_max=round(
int.from_bytes(self._GATE_dBDamping_max, 'little', signed=True) * 0.01,
2,
),
BP_Sidechain=round(
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),

View File

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

View File

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

View File

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