diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..0735471
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,96 @@
+# Changelog
+
+All notable changes to this project will be documented in this file.
+
+The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
+and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+
+Before any major/minor/patch bump all unit tests will be run to verify they pass.
+
+## [Unreleased]
+
+- [ ] Create a stable branch.
+
+## [0.3.0] - 2022-04
+
+### Added
+
+- strip_levels, bus_levels property objects added to base class. These now return the full level array.
+- filter out empty values from strip_levels/bus_levels
+- script decorator added to sendtext() in base class. Now supports passing a nested dict, similar to apply()
+- pre-commit.ps1 added for use with git hook. test badges added to readme.
+- genbadge added to development dependencies in setup.py
+- Lower tests added.
+
+### Changed
+
+- mc getter implemented in strip class
+- bus modes meta function reworked.
+- sendtext() now for multi set operationis (used by apply() method)
+- tests now run according to a kind, for a single run version is random.
+- now using psuedo decorator functions cache_bool and cache_string to handle caching.
+- meta functions reworked.
+- strip bool props moved into factory function.
+
+### Fixed
+
+- fixed size of recvfrom buffer for self.rt_packet_socket in base class
+- nose tests migrated to pytest as nose will not be supported in python 3.10+
+- sendtext() removed from readme. Still in interface but not advised to use since it doesn't update cache.
+
+## [0.2.0] - 2022-03-29
+
+### Added
+
+- profiles module
+- example profiles added to \_profiles/ directory.
+
+### Changed
+
+- setup/teardown moved into login()/logout() functions in base class.
+- now using black formatter, code style badge added to readme.
+
+### Fixed
+
+- bus/strip labels split at null terminator in ascii string.
+- all gainlayers added to isdirty() function in VBAN_VMRT_Packet_Data
+
+## [0.1.0] - 2022-03-21
+
+### Added
+
+- gain property added to strip class.
+- added worker2 thread for keeping the public packet constantly updated in the background.
+- self.running flag for notifying threads when to stop.
+- docstrings added to base class.
+- apply() added to base class and strip/bus classes. supports setting parameters through dict.
+- bus modes mixin added to bus class
+- isdirty() added to VBAN_VMRT_Packet_Data for precisely defining the dirty parameter.
+
+### Changed
+
+- underscore removed from package name. https://peps.python.org/pep-0008/#package-and-module-names
+- readme updated to reflect changes.
+- boolean strip/bus properties now defined by meta functions.
+
+### Fixed
+
+- fixed kind map ins, outs order. (causing error with basic version)
+
+## [0.0.1] - 2022-02
+
+### Added
+
+- Create the base class, setup entry point to interface.
+- worker thread added to keep interface registered to the rt packet service
+- Added definitions for rt packet data and the various packet headers, as dataclasses.
+- Property objects in data packet dataclass for returning byte tuples/parsing string params.
+- Adding kinds module for mapping each Voicemeeter version to a namedtuple.
+- Added meta module.
+- Strip/Bus modules added.
+- Modes dataclass for defining strip states through bit modes.
+- GainLayer added to strip module. gainlayer properties added as mixin.
+- Higher unit tests added.
+- show(), hide(), shutdown() and restart() added to base class.
+- Add initial version of readme.
+- add property objects sr, nbc and streamname to TextRequestHeader. Now settable by kwargs.
diff --git a/README.md b/README.md
index 5fa0da4..1c470b8 100644
--- a/README.md
+++ b/README.md
@@ -6,12 +6,16 @@
# VBAN CMD
-This package offers a Python interface for [Voicemeeter VBAN TEXT](https://vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf#page=19) as well as the [Voicemeeter RT Packet Service](https://vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf#page=27) which allows a client to send and receive parameter values over a local network.
+This package offers a Python interface for the Voicemeeter RT Packet Service as well as Voicemeeter VBAN-TEXT.
+
+This allows a user to get (rt packets) and set (vban-text) parameters over a local network. Consider the Streamer View app over VBAN, for example.
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
For sending audio across a network with VBAN you will need to look elsewhere.
+For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
+
## Tested against
- Basic 1.0.8.2
@@ -20,7 +24,7 @@ For sending audio across a network with VBAN you will need to look elsewhere.
## Prerequisites
-- Voicemeeter 1 (Basic), 2 (Banana) or 3 (Potato)
+- [Voicemeeter](https://voicemeeter.com/)
- Python 3.9+
## Installation
@@ -46,7 +50,7 @@ pip install -e .['development']
#### Use with a context manager:
-Parameter coverage is not as extensive for the RT Packet Service as with the Remote API.
+Parameter coverage is not as extensive for this interface as with the Remote API.
### Example 1
@@ -82,8 +86,6 @@ if __name__ == '__main__':
#### Or perform setup/teardown independently:
-for example:
-
### Example 2
```python
@@ -267,6 +269,10 @@ vban.bus[0].mode.tvmix = True
### `VbanCmd` (lower level)
+#### `vban.pdirty`
+
+True iff a parameter has been changed. Typically this is checked periodically to update states.
+
#### `vban.set_rt(id_, param, val)`
Sends a string request RT Packet where the command would take the form:
@@ -275,22 +281,9 @@ Sends a string request RT Packet where the command would take the form:
f'{id_}.{param}={val}'
```
-#### `vban._get_rt()`
+#### `vban.public_packet`
-Used for updating the RT data packet, used internally by the Interface.
-
-```python
-vban.public_packet = vban._get_rt()
-```
-
-#### `vban.sendtext(cmd)`
-
-Sends a multi parameter TEXT string command, for example:
-
-```python
-# Use ';' or ',' for delimiters.
-vban.sendtext('Strip[0].Mute=1;Strip[3].A3=0;Bus[2].Mute=0;Bus[3].Eq.On=1')
-```
+Returns a Voicemeeter rt data packet. Designed to be used internally by the interface but available for parsing through this read only property object. States may or may not be current, use the polling parameter pdirty to be sure.
### `Errors`
@@ -306,4 +299,6 @@ Then from tests directory:
## Resources
-- [Voicemeeter RT Packet Service](https://vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf)
+- [Voicemeeter VBAN TEXT](https://vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf#page=19)
+
+- [Voicemeeter RT Packet Service](https://vb-audio.com/Voicemeeter/VBANProtocol_Specifications.pdf#page=27)
diff --git a/setup.py b/setup.py
index 8ea0cb6..071842a 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@ from setuptools import setup
setup(
name="vbancmd",
- version="0.0.1",
+ version="0.3.0",
description="VBAN CMD Python API",
packages=["vbancmd"],
install_requires=["toml"],
diff --git a/tests/__init__.py b/tests/__init__.py
index d9a72e9..febfbd6 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -5,7 +5,7 @@ import random
import sys
# let's keep things random
-kind_id = random.choice(("basic", "banana", "potato"))
+kind_id = random.choice(tuple(kind.id for kind in kinds.all))
opts = {
"ip": "codey.local",
diff --git a/tests/banana.svg b/tests/banana.svg
index 7e3c9cc..37c07b2 100644
--- a/tests/banana.svg
+++ b/tests/banana.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/basic.svg b/tests/basic.svg
index 7e3c9cc..37c07b2 100644
--- a/tests/basic.svg
+++ b/tests/basic.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/potato.svg b/tests/potato.svg
index 51143d6..093e718 100644
--- a/tests/potato.svg
+++ b/tests/potato.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/tests/pre-commit.ps1 b/tests/pre-commit.ps1
index deb12b0..53f5bbd 100644
--- a/tests/pre-commit.ps1
+++ b/tests/pre-commit.ps1
@@ -3,12 +3,12 @@ Function RunTests {
$run_tests = "pytest -v --capture=tee-sys --junitxml=./tests/.coverage.xml"
$match_pattern = "^=|^\s*$|^Running|^Using|^plugins|^collecting|^tests"
- Clear-Content $coverage
+ if ( Test-Path $coverage ) { Clear-Content $coverage }
ForEach ($line in $(Invoke-Expression $run_tests)) {
If ( $line -Match $match_pattern ) {
if ( $line -Match "^Running tests for kind \[(\w+)\]" ) { $kind = $Matches[1] }
- $line | Tee-Object -FilePath $coverage -Append
+ $line | Tee-Object -FilePath $coverage -Append
}
}
Write-Output "$(Get-TimeStamp)" | Out-file $coverage -Append
@@ -17,9 +17,9 @@ Function RunTests {
}
Function Get-TimeStamp {
-
+
return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
-
+
}
if ($MyInvocation.InvocationName -ne ".") {
diff --git a/tests/test_lower.py b/tests/test_lower.py
index 40e54d9..43d0f39 100644
--- a/tests/test_lower.py
+++ b/tests/test_lower.py
@@ -1,79 +1,32 @@
import pytest
from tests import tests, data
+from vbancmd import kinds
+import re
-class TestSetAndGetFloatLower:
- __test__ = False
+class TestPublicPacketLower:
+ __test__ = True
- """VBVMR_SetParameterFloat, VBVMR_GetParameterFloat"""
+ """Tests for a valid rt data packet"""
- @pytest.mark.parametrize(
- "param,value",
- [
- (f"Strip[{data.phys_in}].Mute", 1),
- (f"Bus[{data.virt_out}].Eq.on", 1),
- (f"Strip[{data.phys_in}].Mute", 0),
- (f"Bus[{data.virt_out}].Eq.on", 0),
- ],
- )
- def test_it_sets_and_gets_mute_eq_float_params(self, param, value):
- tests.set(param, value)
- assert (round(tests.get(param))) == value
-
- @pytest.mark.parametrize(
- "param,value",
- [
- (f"Strip[{data.phys_in}].Comp", 5.3),
- (f"Strip[{data.virt_in}].Gain", -37.5),
- (f"Bus[{data.virt_out}].Gain", -22.7),
- ],
- )
- def test_it_sets_and_gets_comp_gain_float_params(self, param, value):
- tests.set(param, value)
- assert (round(tests.get(param), 1)) == value
-
-
-@pytest.mark.parametrize("value", ["test0", "test1"])
-class TestSetAndGetStringLower:
- __test__ = False
-
- """VBVMR_SetParameterStringW, VBVMR_GetParameterStringW"""
-
- @pytest.mark.parametrize(
- "param",
- [(f"Strip[{data.phys_out}].label"), (f"Bus[{data.virt_out}].label")],
- )
- def test_it_sets_and_gets_string_params(self, param, value):
- tests.set(param, value)
- assert tests.get(param, string=True) == value
+ def test_it_gets_an_rt_data_packet(self):
+ assert tests.public_packet.voicemeetertype in (kind.id for kind in kinds.all)
@pytest.mark.parametrize("value", [0, 1])
-class TestMacroButtonsLower:
- __test__ = False
+class TestSetRT:
+ __test__ = True
- """VBVMR_MacroButton_SetStatus, VBVMR_MacroButton_GetStatus"""
+ """Tests set_rt"""
@pytest.mark.parametrize(
- "index, mode",
- [(33, 1), (49, 1)],
+ "kls,index,param",
+ [
+ ("strip", data.phys_in, "mute"),
+ ("bus", data.virt_out, "mono"),
+ ],
)
- def test_it_sets_and_gets_macrobuttons_state(self, index, mode, value):
- tests.set_buttonstatus(index, value, mode)
- assert tests.get_buttonstatus(index, mode) == value
-
- @pytest.mark.parametrize(
- "index, mode",
- [(14, 2), (12, 2)],
- )
- def test_it_sets_and_gets_macrobuttons_stateonly(self, index, mode, value):
- tests.set_buttonstatus(index, value, mode)
- assert tests.get_buttonstatus(index, mode) == value
-
- @pytest.mark.parametrize(
- "index, mode",
- [(50, 3), (65, 3)],
- )
- def test_it_sets_and_gets_macrobuttons_trigger(self, index, mode, value):
- tests.set_buttonstatus(index, value, mode)
- assert tests.get_buttonstatus(index, mode) == value
+ def test_it_gets_an_rt_data_packet(self, kls, index, param, value):
+ tests.set_rt(f"{kls}[{index}]", param, value)
+ target = getattr(tests, kls)[index]
+ assert getattr(target, param) == bool(value)
diff --git a/vbancmd/bus.py b/vbancmd/bus.py
index 3bfec33..c1f82ba 100644
--- a/vbancmd/bus.py
+++ b/vbancmd/bus.py
@@ -22,6 +22,9 @@ class OutputBus(Channel):
"levels": BusLevel(remote, index),
"mode": BusModeMixin(remote, index),
**{param: channel_bool_prop(param) for param in ["mute", "mono"]},
+ "eq": channel_bool_prop("eq.On"),
+ "eq_ab": channel_bool_prop("eq.ab"),
+ "label": channel_label_prop(),
},
)
return OB_cls(remote, index, *args, **kwargs)
@@ -30,12 +33,6 @@ class OutputBus(Channel):
def identifier(self):
return "bus"
- eq = channel_bool_prop("eq.On")
-
- eq_ab = channel_bool_prop("eq.ab")
-
- label = channel_label_prop()
-
@property
def gain(self) -> float:
def fget():
diff --git a/vbancmd/channel.py b/vbancmd/channel.py
index 7af1b4d..3880fb6 100644
--- a/vbancmd/channel.py
+++ b/vbancmd/channel.py
@@ -103,8 +103,12 @@ class Channel(abc.ABC):
return self._remote.public_packet
def apply(self, mapping):
- """Sets all parameters of a dict for the strip."""
+ """Sets all parameters of a dict for the channel."""
+ script = ""
for key, val in mapping.items():
if not hasattr(self, key):
raise VMCMDErrors(f"Invalid {self.identifier} attribute: {key}")
- setattr(self, key, val)
+ self._remote.cache[f"{self.identifier}[{self.index}].{key}"] = val
+ script += f"{self.identifier}[{self.index}].{key}={val};"
+
+ self._remote.sendtext(script)
diff --git a/vbancmd/meta.py b/vbancmd/meta.py
index 64f3ac2..d84bc03 100644
--- a/vbancmd/meta.py
+++ b/vbancmd/meta.py
@@ -14,7 +14,7 @@ def channel_bool_prop(param):
getattr(self.public_packet, f"{self.identifier}state")[self.index],
"little",
)
- & getattr(self._modes, f"_{param}")
+ & getattr(self._modes, f'_{param.replace(".", "_").lower()}')
== 0
)
diff --git a/vbancmd/strip.py b/vbancmd/strip.py
index b5667f8..7d651d5 100644
--- a/vbancmd/strip.py
+++ b/vbancmd/strip.py
@@ -25,6 +25,7 @@ class InputStrip(Channel):
param: channel_bool_prop(param)
for param in ["mono", "solo", "mute"]
},
+ "label": channel_label_prop(),
},
)
return IS_cls(remote, index, **kwargs)
@@ -33,8 +34,6 @@ class InputStrip(Channel):
def identifier(self):
return "strip"
- label = channel_label_prop()
-
@property
def limit(self) -> int:
return
diff --git a/vbancmd/util.py b/vbancmd/util.py
index fc14dff..db7236d 100644
--- a/vbancmd/util.py
+++ b/vbancmd/util.py
@@ -34,6 +34,12 @@ def cache_string(func, param):
return wrapper
+def depth(d):
+ if isinstance(d, dict):
+ return 1 + (max(map(depth, d.values())) if d else 0)
+ return 0
+
+
def script(func):
"""Convert dictionary to script"""
diff --git a/vbancmd/vbancmd.py b/vbancmd/vbancmd.py
index 4a1bd54..4fdeb73 100644
--- a/vbancmd/vbancmd.py
+++ b/vbancmd/vbancmd.py
@@ -67,7 +67,6 @@ class VbanCmd(abc.ABC):
self.running = True
self._pdirty = False
self.cache = {}
- self.in_apply = False
def __enter__(self):
self.login()
@@ -156,10 +155,6 @@ class VbanCmd(abc.ABC):
while self.pdirty:
pass
- @public_packet.setter
- def public_packet(self, val):
- self._public_packet = val
-
def _keepupdated(self) -> NoReturn:
"""
Continously update public packet in background.
@@ -173,8 +168,8 @@ class VbanCmd(abc.ABC):
while self.running:
private_packet = self._get_rt()
self._pdirty = private_packet.isdirty(self.public_packet)
- if not private_packet.__eq__(self.public_packet):
- self.public_packet = private_packet
+ if not private_packet == self.public_packet:
+ self._public_packet = private_packet
def _get_rt(self) -> VBAN_VMRT_Packet_Data:
"""Attempt to fetch data packet until a valid one found"""
@@ -204,8 +199,6 @@ class VbanCmd(abc.ABC):
self._text_header.framecounter = count.to_bytes(4, "little")
if param:
self.cache[f"{id_}.{param}"] = val
- if self._sync or self.in_apply:
- sleep(self._delay)
@script
def sendtext(self, cmd):
@@ -241,7 +234,6 @@ class VbanCmd(abc.ABC):
def apply(self, mapping: dict):
"""Sets all parameters of a di"""
- self.in_apply = True
for key, submapping in mapping.items():
obj, index = key.split("-")
@@ -252,7 +244,6 @@ class VbanCmd(abc.ABC):
else:
raise ValueError(obj)
target.apply(submapping)
- self.in_apply = False
def apply_profile(self, name: str):
try:
@@ -266,7 +257,7 @@ class VbanCmd(abc.ABC):
else:
base[key] = profile[key]
profile = base
- self.sendtext(profile)
+ self.apply(profile)
except KeyError:
raise VMCMDErrors(f"Unknown profile: {self.kind.id}/{name}")