mirror of
https://github.com/onyx-and-iris/vban-cmd-python.git
synced 2026-04-18 21:13:30 +00:00
Compare commits
61 Commits
f6218d2032
...
add-event-
| Author | SHA1 | Date | |
|---|---|---|---|
| cbcca14481 | |||
| f584d53835 | |||
| 72d182a488 | |||
| ee32f92914 | |||
| 3b65035e50 | |||
| c8b4bde49d | |||
| 47e9203b1e | |||
| d48e7ecd79 | |||
| 7e09a0d321 | |||
| d41ee1a12a | |||
| 1e499cd99d | |||
| 9bf52b5c11 | |||
| 77ba347e99 | |||
| 94fa33cebf | |||
| ef105d878b | |||
| 956f759e73 | |||
| dab519be9f | |||
| a4b91bf5c6 | |||
| 2a98707bf8 | |||
| 8e30c57020 | |||
| 04e18b304b | |||
| 4de384c66c | |||
| 2c8659a4e5 | |||
| 41e427e46b | |||
| fc6fdb44b5 | |||
| b49dc3b9b3 | |||
| 1ad0347478 | |||
| 2c8e4cc87c | |||
| fc3b31dfa7 | |||
| 544e0f2a32 | |||
| f6d92d1c34 | |||
| 10dbf63056 | |||
| 6ddd4151b4 | |||
| 8b912a2d08 | |||
| d2a5fe197e | |||
| 0970bfe0b5 | |||
| 54041503c9 | |||
| 9d015755eb | |||
| ca9a31c94a | |||
| 7a3abfc372 | |||
| 37a9c88867 | |||
| df7996a846 | |||
| 3f5dc7c376 | |||
| 05cbc432b2 | |||
| 174d95d08d | |||
| fc324fecc4 | |||
| 449cb9b3c1 | |||
| cdccc603d1 | |||
| a8bb9711af | |||
| 5bb0c2731e | |||
| 372dba0b6b | |||
| 226fc5ead7 | |||
| 9196a4e267 | |||
| 8485992495 | |||
| 91e49cbb55 | |||
| 3c85903554 | |||
| a730edc2c2 | |||
| 90acafe95b | |||
| 5f4fdcb0eb | |||
| d5219d66f7 | |||
| c74d827154 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -85,7 +85,7 @@ ipython_config.py
|
|||||||
# pyenv
|
# pyenv
|
||||||
# For a library or package, you might want to ignore these files since the code is
|
# For a library or package, you might want to ignore these files since the code is
|
||||||
# intended to run in multiple environments; otherwise, check them in:
|
# intended to run in multiple environments; otherwise, check them in:
|
||||||
# .python-version
|
.python-version
|
||||||
|
|
||||||
# pipenv
|
# pipenv
|
||||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||||
@@ -157,3 +157,5 @@ quick.py
|
|||||||
#config
|
#config
|
||||||
config.toml
|
config.toml
|
||||||
vban.toml
|
vban.toml
|
||||||
|
|
||||||
|
.vscode/
|
||||||
69
CHANGELOG.md
69
CHANGELOG.md
@@ -11,6 +11,75 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
|||||||
|
|
||||||
- [x]
|
- [x]
|
||||||
|
|
||||||
|
## [2.3.2] - 2023-07-12
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- vban.{instream,outstream} tuples now contain classes that represent MIDI and TEXT streams.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- apply_config() now performs a deep merge when extending a config with another.
|
||||||
|
|
||||||
|
## [2.3.0] - 2023-07-11
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- user configs may now extend other user configs. check `config extends` section in README.
|
||||||
|
|
||||||
|
## [2.2.0] - 2023-07-08
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- button, vban classes implemented
|
||||||
|
- \__repr\__() method added to base class
|
||||||
|
|
||||||
|
## [2.1.2] - 2023-07-05
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `outbound` kwarg let's you disable incoming rt packets. Essentially the interface will work only in one direction.
|
||||||
|
|
||||||
|
This is useful if you are only interested in sending commands out to voicemeeter but don't need to receive parameter states.
|
||||||
|
|
||||||
|
By default outbound is False.
|
||||||
|
|
||||||
|
- sendtext logging added in base class.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Bug in apply() if invoked from a higher class (not base class)
|
||||||
|
|
||||||
|
## [2.0.0] - 2023-06-25
|
||||||
|
|
||||||
|
This update introduces some breaking changes:
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- `strip[i].comp` now references StripComp class
|
||||||
|
- To change the comp knob you should now use the property `strip[i].comp.knob`
|
||||||
|
- `strip[i].gate` now references StripGate class
|
||||||
|
|
||||||
|
- To change the gate knob you should now use the property `strip[i].gate.knob`
|
||||||
|
|
||||||
|
- `bus[i].eq` now references BusEQ class
|
||||||
|
|
||||||
|
- To set bus[i].{eq,eq_ab} as before you should now use bus[i].eq.on and bus[i].eq.ab
|
||||||
|
|
||||||
|
- new error class `VBANCMDConnectionError` raised when a connection fails or times out.
|
||||||
|
|
||||||
|
There are other non-breaking changes:
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- now using a producer thread to send events to the updater thread.
|
||||||
|
- factory.request_vbancmd_obj simply raises a `VBANCMDError` if passed an incorrect kind.
|
||||||
|
- module level loggers implemented (with class loggers as child loggers)
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `strip[i].eq` added to PhysicalStrip
|
||||||
|
|
||||||
## [1.8.0]
|
## [1.8.0]
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
170
README.md
170
README.md
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
# VBAN CMD
|
# VBAN CMD
|
||||||
|
|
||||||
This python interface allows you to get and set Voicemeeter parameter values over a network.
|
This python interface allows you to transmit Voicemeeter parameters over a network.
|
||||||
|
|
||||||
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
|
It may be used standalone or to extend the [Voicemeeter Remote Python API](https://github.com/onyx-and-iris/voicemeeter-api-python)
|
||||||
|
|
||||||
@@ -18,9 +18,9 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
|||||||
|
|
||||||
## Tested against
|
## Tested against
|
||||||
|
|
||||||
- Basic 1.0.8.4
|
- Basic 1.0.8.8
|
||||||
- Banana 2.0.6.4
|
- Banana 2.0.6.8
|
||||||
- Potato 3.0.2.4
|
- Potato 3.0.2.8
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ port = 6980
|
|||||||
streamname = "Command1"
|
streamname = "Command1"
|
||||||
```
|
```
|
||||||
|
|
||||||
It should be placed next to your `__main__.py` file.
|
It should be placed in \<user home directory\> / "Documents" / "Voicemeeter" / "configs"
|
||||||
|
|
||||||
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
Alternatively you may pass `ip`, `port`, `streamname` as keyword arguments.
|
||||||
|
|
||||||
@@ -71,19 +71,19 @@ class ManyThings:
|
|||||||
|
|
||||||
def other_things(self):
|
def other_things(self):
|
||||||
self.vban.bus[3].gain = -6.3
|
self.vban.bus[3].gain = -6.3
|
||||||
self.vban.bus[4].eq = True
|
self.vban.bus[4].eq.on = True
|
||||||
info = (
|
info = (
|
||||||
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
f"bus 3 gain has been set to {self.vban.bus[3].gain}",
|
||||||
f"bus 4 eq has been set to {self.vban.bus[4].eq}",
|
f"bus 4 eq has been set to {self.vban.bus[4].eq.on}",
|
||||||
)
|
)
|
||||||
print("\n".join(info))
|
print("\n".join(info))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
kind_id = "banana"
|
KIND_ID = "banana"
|
||||||
|
|
||||||
with vban_cmd.api(
|
with vban_cmd.api(
|
||||||
kind_id, ip="gamepc.local", port=6980, streamname="Command1"
|
KIND_ID, ip="gamepc.local", port=6980, streamname="Command1"
|
||||||
) as vban:
|
) as vban:
|
||||||
do = ManyThings(vban)
|
do = ManyThings(vban)
|
||||||
do.things()
|
do.things()
|
||||||
@@ -93,7 +93,7 @@ def main():
|
|||||||
vban.apply(
|
vban.apply(
|
||||||
{
|
{
|
||||||
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
"strip-2": {"A1": True, "B1": True, "gain": -6.0},
|
||||||
"bus-2": {"mute": True},
|
"bus-2": {"mute": True, "eq": {"on": True}},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -104,9 +104,9 @@ if __name__ == "__main__":
|
|||||||
|
|
||||||
Otherwise you must remember to call `vban.login()`, `vban.logout()` at the start/end of your code.
|
Otherwise you must remember to call `vban.login()`, `vban.logout()` at the start/end of your code.
|
||||||
|
|
||||||
## `kind_id`
|
## `KIND_ID`
|
||||||
|
|
||||||
Pass the kind of Voicemeeter as an argument. kind_id may be:
|
Pass the kind of Voicemeeter as an argument. KIND_ID may be:
|
||||||
|
|
||||||
- `basic`
|
- `basic`
|
||||||
- `banana`
|
- `banana`
|
||||||
@@ -124,8 +124,6 @@ The following properties are available.
|
|||||||
- `label`: string
|
- `label`: string
|
||||||
- `gain`: float, -60 to 12
|
- `gain`: float, -60 to 12
|
||||||
- `A1 - A5`, `B1 - B3`: boolean
|
- `A1 - A5`, `B1 - B3`: boolean
|
||||||
- `comp`: float, from 0.0 to 10.0
|
|
||||||
- `gate`: float, from 0.0 to 10.0
|
|
||||||
- `limit`: int, from -40 to 12
|
- `limit`: int, from -40 to 12
|
||||||
|
|
||||||
example:
|
example:
|
||||||
@@ -152,6 +150,69 @@ vban.strip[5].appmute("Spotify", True)
|
|||||||
vban.strip[5].appgain("Spotify", 0.5)
|
vban.strip[5].appgain("Spotify", 0.5)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Strip.Comp
|
||||||
|
|
||||||
|
The following properties are available.
|
||||||
|
|
||||||
|
- `knob`: float, from 0.0 to 10.0
|
||||||
|
- `gainin`: float, from -24.0 to 24.0
|
||||||
|
- `ratio`: float, from 1.0 to 8.0
|
||||||
|
- `threshold`: float, from -40.0 to -3.0
|
||||||
|
- `attack`: float, from 0.0 to 200.0
|
||||||
|
- `release`: float, from 0.0 to 5000.0
|
||||||
|
- `knee`: float, from 0.0 to 1.0
|
||||||
|
- `gainout`: float, from -24.0 to 24.0
|
||||||
|
- `makeup`: boolean
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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.Gate
|
||||||
|
|
||||||
|
The following properties are available.
|
||||||
|
|
||||||
|
- `knob`: float, from 0.0 to 10.0
|
||||||
|
- `threshold`: float, from -60.0 to -10.0
|
||||||
|
- `damping`: float, from -60.0 to -10.0
|
||||||
|
- `bpsidechain`: int, from 100 to 4000
|
||||||
|
- `attack`: float, from 0.0 to 1000.0
|
||||||
|
- `hold`: float, from 0.0 to 5000.0
|
||||||
|
- `release`: float, from 0.0 to 5000.0
|
||||||
|
|
||||||
|
example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
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.Denoiser
|
||||||
|
|
||||||
|
The following properties are available.
|
||||||
|
|
||||||
|
- `knob`: float, from 0.0 to 10.0
|
||||||
|
|
||||||
|
strip.denoiser properties are defined as write only, potato version only.
|
||||||
|
|
||||||
|
##### Strip.EQ
|
||||||
|
|
||||||
|
The following properties are available.
|
||||||
|
|
||||||
|
- `on`: boolean
|
||||||
|
- `ab`: boolean
|
||||||
|
|
||||||
|
Strip EQ properties are defined as write only, potato version only.
|
||||||
|
|
||||||
##### Gainlayers
|
##### Gainlayers
|
||||||
|
|
||||||
- `gain`: float, from -60.0 to 12.0
|
- `gain`: float, from -60.0 to 12.0
|
||||||
@@ -183,8 +244,6 @@ Level properties will return -200.0 if no audio detected.
|
|||||||
The following properties are available.
|
The following properties are available.
|
||||||
|
|
||||||
- `mono`: boolean
|
- `mono`: boolean
|
||||||
- `eq`: boolean
|
|
||||||
- `eq_ab`: boolean
|
|
||||||
- `mute`: boolean
|
- `mute`: boolean
|
||||||
- `label`: string
|
- `label`: string
|
||||||
- `gain`: float, -60 to 12
|
- `gain`: float, -60 to 12
|
||||||
@@ -192,10 +251,20 @@ The following properties are available.
|
|||||||
example:
|
example:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.bus[4].eq = true
|
|
||||||
print(vban.bus[0].label)
|
print(vban.bus[0].label)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
##### Bus.EQ
|
||||||
|
|
||||||
|
The following properties are available.
|
||||||
|
|
||||||
|
- `on`: boolean
|
||||||
|
- `ab`: boolean
|
||||||
|
|
||||||
|
```python
|
||||||
|
vban.bus[4].eq.on = true
|
||||||
|
```
|
||||||
|
|
||||||
##### Modes
|
##### Modes
|
||||||
|
|
||||||
The following properties are available.
|
The following properties are available.
|
||||||
@@ -285,6 +354,7 @@ vban.apply(
|
|||||||
{
|
{
|
||||||
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
"strip-0": {"A1": True, "B1": True, "gain": -6.0},
|
||||||
"bus-1": {"mute": True, "mode": "composite"},
|
"bus-1": {"mute": True, "mode": "composite"},
|
||||||
|
"bus-2": {"eq": {"on": True}},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
@@ -292,8 +362,8 @@ vban.apply(
|
|||||||
Or for each class you may do:
|
Or for each class you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
vban.strip[0].apply(mute: true, gain: 3.2, A1: true)
|
vban.strip[0].apply({"mute": True, "gain": 3.2, "A1": True})
|
||||||
vban.bus[0].apply(A1: true)
|
vban.vban.outstream[0].apply({"on": True, "name": "streamname", "bit": 24})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Config Files
|
## Config Files
|
||||||
@@ -302,7 +372,7 @@ vban.bus[0].apply(A1: true)
|
|||||||
|
|
||||||
You may load config files in TOML format.
|
You may load config files in TOML format.
|
||||||
Three example configs have been included with the package. Remember to save
|
Three example configs have been included with the package. Remember to save
|
||||||
current settings before loading a user config. To set one you may do:
|
current settings before loading a user config. To load one you may do:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
@@ -312,6 +382,27 @@ with vban_cmd.api('banana') as vban:
|
|||||||
|
|
||||||
will load a config file at configs/banana/example.toml for Voicemeeter Banana.
|
will load a config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||||
|
|
||||||
|
Your configs may be located in one of the following paths:
|
||||||
|
- \<current working directory\> / "configs" / kind_id
|
||||||
|
- \<user home directory\> / ".config" / "vban-cmd" / kind_id
|
||||||
|
- \<user home directory\> / "Documents" / "Voicemeeter" / "configs" / kind_id
|
||||||
|
|
||||||
|
If a config with the same name is located in multiple locations, only the first one found is loaded into memory, in the above order.
|
||||||
|
|
||||||
|
#### `config extends`
|
||||||
|
|
||||||
|
You may also load a config that extends another config with overrides or additional parameters.
|
||||||
|
|
||||||
|
You just need to define a key `extends` in the config TOML, that names the config to be extended.
|
||||||
|
|
||||||
|
Three example 'extender' configs are included with the repo. You may load them with:
|
||||||
|
|
||||||
|
```python
|
||||||
|
import voicemeeterlib
|
||||||
|
with voicemeeterlib.api('banana') as vm:
|
||||||
|
vm.apply_config('extender')
|
||||||
|
```
|
||||||
|
|
||||||
## Events
|
## Events
|
||||||
|
|
||||||
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
Level updates are considered high volume, by default they are NOT listened for. Use `subs` keyword arg to initialize event updates.
|
||||||
@@ -320,14 +411,12 @@ example:
|
|||||||
|
|
||||||
```python
|
```python
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
# Listen for level updates
|
|
||||||
opts = {
|
opts = {
|
||||||
"ip": "<ip address>",
|
"ip": "<ip address>",
|
||||||
"streamname": "Command1",
|
"streamname": "Command1",
|
||||||
"port": 6980,
|
"port": 6980,
|
||||||
"subs": {"ldirty": True},
|
|
||||||
}
|
}
|
||||||
with vban_cmd.api('banana', **opts) as vban:
|
with vban_cmd.api('banana', ldirty=True, **opts) as vban:
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -386,16 +475,17 @@ print(vban.event.get())
|
|||||||
|
|
||||||
## VbanCmd class
|
## VbanCmd class
|
||||||
|
|
||||||
`vban_cmd.api(kind_id: str, **opts: dict)`
|
`vban_cmd.api(kind_id: str, **opts)`
|
||||||
|
|
||||||
You may pass the following optional keyword arguments:
|
You may pass the following optional keyword arguments:
|
||||||
|
|
||||||
- `ip`: str, ip or hostname of remote machine
|
- `ip`: str, ip or hostname of remote machine
|
||||||
- `streamname`: str, name of the stream to connect to.
|
- `streamname`: str, name of the stream to connect to.
|
||||||
- `port`: int=6980, vban udp port of remote machine.
|
- `port`: int=6980, vban udp port of remote machine.
|
||||||
- `subs`: dict={"pdirty": True, "ldirty": False}, controls which updates to listen for.
|
- `pdirty`: boolean=False, parameter updates
|
||||||
- `pdirty`: parameter updates
|
- `ldirty`: boolean=False, level updates
|
||||||
- `ldirty`: level updates
|
- `timeout`: int=5, amount of time (seconds) to wait for an incoming RT data packet (parameter states).
|
||||||
|
- `outbound`: boolean=False, set `True` if you are only interested in sending commands. (no rt packets will be received)
|
||||||
|
|
||||||
#### `vban.pdirty`
|
#### `vban.pdirty`
|
||||||
|
|
||||||
@@ -415,13 +505,31 @@ vban.sendtext("Strip[0].Mute=1;Bus[0].Mono=1")
|
|||||||
|
|
||||||
#### `vban.public_packet`
|
#### `vban.public_packet`
|
||||||
|
|
||||||
Returns a Voicemeeter rt data packet object. Designed to be used internally by the interface but available for parsing through this read only property object. States not guaranteed to be current (requires use of dirty parameters to confirm).
|
Returns a `VbanRtPacket`. Designed to be used internally by the interface but available for parsing through this read only property object.
|
||||||
|
|
||||||
### `Errors`
|
States not guaranteed to be current (requires use of dirty parameters to confirm).
|
||||||
|
|
||||||
- `errors.VBANCMDError`: Base VMCMD error class.
|
## Errors
|
||||||
|
|
||||||
### `Tests`
|
- `errors.VBANCMDError`: Exception raised when general errors occur.
|
||||||
|
- `errors.VBANCMDConnectionError`: Exception raised when connection/timeout errors occur.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
It's possible to see the messages sent by the interface's setters and getters, may be useful for debugging.
|
||||||
|
|
||||||
|
example:
|
||||||
|
```python
|
||||||
|
import vban_cmd
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
opts = {"ip": "ip.local", "port": 6980, "streamname": "Command1"}
|
||||||
|
with vban_cmd.api('banana', **opts) as vban:
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
First make sure you installed the [development dependencies](https://github.com/onyx-and-iris/vban-cmd-python#installation)
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
label = "PhysStrip0"
|
label = "PhysStrip0"
|
||||||
A1 = true
|
A1 = true
|
||||||
gain = -8.8
|
gain = -8.8
|
||||||
comp = 3.2
|
comp.knob = 3.2
|
||||||
|
|
||||||
[strip-1]
|
[strip-1]
|
||||||
label = "PhysStrip1"
|
label = "PhysStrip1"
|
||||||
B1 = true
|
B1 = true
|
||||||
gate = 4.1
|
gate.knob = 4.1
|
||||||
|
|
||||||
[strip-2]
|
[strip-2]
|
||||||
label = "PhysStrip2"
|
label = "PhysStrip2"
|
||||||
@@ -31,12 +31,12 @@ mono = true
|
|||||||
|
|
||||||
[bus-2]
|
[bus-2]
|
||||||
label = "PhysBus2"
|
label = "PhysBus2"
|
||||||
eq = true
|
eq.on = true
|
||||||
mode = "composite"
|
mode = "composite"
|
||||||
|
|
||||||
[bus-3]
|
[bus-3]
|
||||||
label = "VirtBus0"
|
label = "VirtBus0"
|
||||||
eq_ab = true
|
eq.ab = true
|
||||||
mode = "upmix61"
|
mode = "upmix61"
|
||||||
|
|
||||||
[bus-4]
|
[bus-4]
|
||||||
|
|||||||
12
configs/banana/extender.toml
Normal file
12
configs/banana/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
12
configs/basic/extender.toml
Normal file
12
configs/basic/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
@@ -2,12 +2,12 @@
|
|||||||
label = "PhysStrip0"
|
label = "PhysStrip0"
|
||||||
A1 = true
|
A1 = true
|
||||||
gain = -8.8
|
gain = -8.8
|
||||||
comp = 3.2
|
comp.knob = 3.2
|
||||||
|
|
||||||
[strip-1]
|
[strip-1]
|
||||||
label = "PhysStrip1"
|
label = "PhysStrip1"
|
||||||
B1 = true
|
B1 = true
|
||||||
gate = 4.1
|
gate.knob = 4.1
|
||||||
|
|
||||||
[strip-2]
|
[strip-2]
|
||||||
label = "PhysStrip2"
|
label = "PhysStrip2"
|
||||||
@@ -47,7 +47,7 @@ mono = true
|
|||||||
|
|
||||||
[bus-2]
|
[bus-2]
|
||||||
label = "PhysBus2"
|
label = "PhysBus2"
|
||||||
eq = true
|
eq.on = true
|
||||||
|
|
||||||
[bus-3]
|
[bus-3]
|
||||||
label = "PhysBus3"
|
label = "PhysBus3"
|
||||||
@@ -59,7 +59,7 @@ mode = "composite"
|
|||||||
|
|
||||||
[bus-5]
|
[bus-5]
|
||||||
label = "VirtBus0"
|
label = "VirtBus0"
|
||||||
eq_ab = true
|
eq.ab = true
|
||||||
|
|
||||||
[bus-6]
|
[bus-6]
|
||||||
label = "VirtBus1"
|
label = "VirtBus1"
|
||||||
|
|||||||
12
configs/potato/extender.toml
Normal file
12
configs/potato/extender.toml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
extends = "example"
|
||||||
|
[strip-0]
|
||||||
|
label = "strip0_extended"
|
||||||
|
A1 = false
|
||||||
|
gain = 0.0
|
||||||
|
|
||||||
|
[bus-0]
|
||||||
|
label = "bus0_extended"
|
||||||
|
mute = false
|
||||||
|
|
||||||
|
[vban-in-3]
|
||||||
|
name = "vban_extended"
|
||||||
13
examples/gui/README.md
Normal file
13
examples/gui/README.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
## About
|
||||||
|
|
||||||
|
A single channel GUI demonstrating controls for the first virtual strip if Voicemeeter Banana.
|
||||||
|
|
||||||
|
This example demonstrates (to an extent) two way communication.
|
||||||
|
- Sending parameters values to the Voicemeeter driver.
|
||||||
|
- Receiving level updates
|
||||||
|
|
||||||
|
Parameter updates (pdirty) events are not being received so changing a UI element on the main Voicemeeter app will not be reflected in the example GUI.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
Simply run the script and try the controls.
|
||||||
109
examples/gui/__main__.py
Normal file
109
examples/gui/__main__.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
import vban_cmd
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
|
class App(tk.Tk):
|
||||||
|
INDEX = 3
|
||||||
|
|
||||||
|
def __init__(self, vban):
|
||||||
|
super().__init__()
|
||||||
|
self.vban = vban
|
||||||
|
self.title(f"{vban} - version {vban.version}")
|
||||||
|
self.vban.observer.add(self.on_ldirty)
|
||||||
|
|
||||||
|
# create widget variables
|
||||||
|
self.button_var = tk.BooleanVar(value=vban.strip[self.INDEX].mute)
|
||||||
|
self.slider_var = tk.DoubleVar(value=vban.strip[self.INDEX].gain)
|
||||||
|
self.meter_var = tk.DoubleVar(value=self._get_level())
|
||||||
|
self.gainlabel_var = tk.StringVar(value=self.slider_var.get())
|
||||||
|
|
||||||
|
# initialize style table
|
||||||
|
self.style = ttk.Style()
|
||||||
|
self.style.theme_use("clam")
|
||||||
|
self.style.configure(
|
||||||
|
"Mute.TButton",
|
||||||
|
foreground="#cd5c5c" if vban.strip[self.INDEX].mute else "#5a5a5a",
|
||||||
|
)
|
||||||
|
|
||||||
|
# create labelframe and grid it onto the mainframe
|
||||||
|
self.labelframe = tk.LabelFrame(text=self.vban.strip[self.INDEX].label)
|
||||||
|
self.labelframe.grid(padx=1)
|
||||||
|
|
||||||
|
# create slider and grid it onto the labelframe
|
||||||
|
slider = ttk.Scale(
|
||||||
|
self.labelframe,
|
||||||
|
from_=12,
|
||||||
|
to_=-60,
|
||||||
|
orient="vertical",
|
||||||
|
variable=self.slider_var,
|
||||||
|
command=lambda arg: self.on_slider_move(arg),
|
||||||
|
)
|
||||||
|
slider.grid(
|
||||||
|
column=0,
|
||||||
|
row=0,
|
||||||
|
)
|
||||||
|
slider.bind("<Double-Button-1>", self.on_button_double_click)
|
||||||
|
|
||||||
|
# create level meter and grid it onto the labelframe
|
||||||
|
level_meter = ttk.Progressbar(
|
||||||
|
self.labelframe,
|
||||||
|
orient="vertical",
|
||||||
|
variable=self.meter_var,
|
||||||
|
maximum=72,
|
||||||
|
mode="determinate",
|
||||||
|
)
|
||||||
|
level_meter.grid(column=1, row=0)
|
||||||
|
|
||||||
|
# create gainlabel and grid it onto the labelframe
|
||||||
|
gainlabel = ttk.Label(self.labelframe, textvariable=self.gainlabel_var)
|
||||||
|
gainlabel.grid(column=0, row=1, columnspan=2)
|
||||||
|
|
||||||
|
# create button and grid it onto the labelframe
|
||||||
|
button = ttk.Button(
|
||||||
|
self.labelframe,
|
||||||
|
text="Mute",
|
||||||
|
style="Mute.TButton",
|
||||||
|
command=lambda: self.on_button_press(),
|
||||||
|
)
|
||||||
|
button.grid(column=0, row=2, columnspan=2, padx=1, pady=2)
|
||||||
|
|
||||||
|
# define callbacks
|
||||||
|
|
||||||
|
def on_slider_move(self, *args):
|
||||||
|
val = round(self.slider_var.get(), 1)
|
||||||
|
self.vban.strip[self.INDEX].gain = val
|
||||||
|
self.gainlabel_var.set(val)
|
||||||
|
|
||||||
|
def on_button_press(self):
|
||||||
|
self.button_var.set(not self.button_var.get())
|
||||||
|
self.vban.strip[self.INDEX].mute = self.button_var.get()
|
||||||
|
self.style.configure(
|
||||||
|
"Mute.TButton", foreground="#cd5c5c" if self.button_var.get() else "#5a5a5a"
|
||||||
|
)
|
||||||
|
|
||||||
|
def on_button_double_click(self, e):
|
||||||
|
self.slider_var.set(0)
|
||||||
|
self.gainlabel_var.set(0)
|
||||||
|
self.vban.strip[self.INDEX].gain = 0
|
||||||
|
|
||||||
|
def _get_level(self):
|
||||||
|
val = max(self.vban.strip[self.INDEX].levels.prefader)
|
||||||
|
return 0 if self.button_var.get() else 72 + val - 12 + self.slider_var.get()
|
||||||
|
|
||||||
|
def on_ldirty(self):
|
||||||
|
self.meter_var.set(self._get_level())
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with vban_cmd.api("banana", ldirty=True) as vban:
|
||||||
|
app = App(vban)
|
||||||
|
app.mainloop()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -40,10 +40,12 @@ Make sure you have established a working connection to OBS and the remote Voicem
|
|||||||
|
|
||||||
Run the script, change OBS scenes and watch Voicemeeter parameters change.
|
Run the script, change OBS scenes and watch Voicemeeter parameters change.
|
||||||
|
|
||||||
Pressing `<Enter>` will exit.
|
Closing OBS will end the script.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
All but `vban_cmd.iremote` logs are filtered out. Log in DEBUG mode.
|
||||||
|
|
||||||
This script can be run from a Linux host since the vban-cmd interface relies on UDP packets and obsws-python runs over websockets.
|
This script can be run from a Linux host since the vban-cmd interface relies on UDP packets and obsws-python runs over websockets.
|
||||||
|
|
||||||
You could for example, set this up to run in the background on a home server such as a Raspberry Pi.
|
You could for example, set this up to run in the background on a home server such as a Raspberry Pi.
|
||||||
|
|||||||
@@ -1,16 +1,41 @@
|
|||||||
import logging
|
import time
|
||||||
|
from logging import config
|
||||||
|
|
||||||
|
import obsws_python as obsws
|
||||||
|
|
||||||
import obsws_python as obs
|
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
config.dictConfig(
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"formatters": {
|
||||||
|
"standard": {
|
||||||
|
"format": "%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"handlers": {
|
||||||
|
"stream": {
|
||||||
|
"level": "DEBUG",
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "standard",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"loggers": {"vban_cmd.iremote": {"handlers": ["stream"], "level": "DEBUG"}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class Observer:
|
class Observer:
|
||||||
def __init__(self, vban):
|
def __init__(self, vban):
|
||||||
self.vban = vban
|
self.vban = vban
|
||||||
self.client = obs.EventClient()
|
self.client = obsws.EventClient()
|
||||||
self.client.callback.register(self.on_current_program_scene_changed)
|
self.client.callback.register(
|
||||||
|
(
|
||||||
|
self.on_current_program_scene_changed,
|
||||||
|
self.on_exit_started,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.is_running = True
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
self.vban.strip[0].mute = True
|
self.vban.strip[0].mute = True
|
||||||
@@ -50,13 +75,16 @@ class Observer:
|
|||||||
if fn := fget(scene):
|
if fn := fget(scene):
|
||||||
fn()
|
fn()
|
||||||
|
|
||||||
|
def on_exit_started(self, _):
|
||||||
|
self.client.unsubscribe()
|
||||||
|
self.is_running = False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
with vban_cmd.api("potato", sync=True) as vban:
|
with vban_cmd.api("potato") as vban:
|
||||||
obs = Observer(vban)
|
observer = Observer(vban)
|
||||||
while cmd := input("<Enter> to exit\n"):
|
while observer.is_running:
|
||||||
if not cmd:
|
time.sleep(0.1)
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
7
examples/obs/setup.py
Normal file
7
examples/obs/setup.py
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
setup(
|
||||||
|
name="obs",
|
||||||
|
description="OBS Example",
|
||||||
|
install_requires=["obsws-python"],
|
||||||
|
)
|
||||||
@@ -5,33 +5,30 @@ import vban_cmd
|
|||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Observer:
|
class App:
|
||||||
def __init__(self, vban):
|
def __init__(self, vban):
|
||||||
self.vban = vban
|
self.vban = vban
|
||||||
# register your app as event observer
|
# register your app as event observer
|
||||||
self.vban.subject.add(self)
|
self.vban.observer.add(self)
|
||||||
# enable level updates, since they are disabled by default.
|
|
||||||
self.vban.event.ldirty = True
|
|
||||||
|
|
||||||
# define an 'on_update' callback function to receive event updates
|
# define an 'on_update' callback function to receive event updates
|
||||||
def on_update(self, subject):
|
def on_update(self, event):
|
||||||
if subject == "pdirty":
|
if event == "pdirty":
|
||||||
print("pdirty!")
|
print("pdirty!")
|
||||||
elif subject == "ldirty":
|
elif event == "ldirty":
|
||||||
for bus in self.vban.bus:
|
for bus in self.vban.bus:
|
||||||
if bus.levels.isdirty:
|
if bus.levels.isdirty:
|
||||||
print(bus, bus.levels.all)
|
print(bus, bus.levels.all)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
kind_id = "potato"
|
KIND_ID = "banana"
|
||||||
|
|
||||||
with vban_cmd.api(kind_id) as vban:
|
with vban_cmd.api(KIND_ID, pdirty=True, ldirty=True) as vban:
|
||||||
Observer(vban)
|
App(vban)
|
||||||
|
|
||||||
while cmd := input("Press <Enter> to exit\n"):
|
while cmd := input("Press <Enter> to exit\n"):
|
||||||
if not cmd:
|
pass
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
125
poetry.lock
generated
125
poetry.lock
generated
@@ -33,6 +33,22 @@ d = ["aiohttp (>=3.7.4)"]
|
|||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cachetools"
|
||||||
|
version = "5.3.1"
|
||||||
|
description = "Extensible memoizing collections and decorators"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chardet"
|
||||||
|
version = "5.1.0"
|
||||||
|
description = "Universal encoding detector for Python 3"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.3"
|
version = "8.1.3"
|
||||||
@@ -46,11 +62,31 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.5"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "distlib"
|
||||||
|
version = "0.3.6"
|
||||||
|
description = "Distribution utilities"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "filelock"
|
||||||
|
version = "3.12.2"
|
||||||
|
description = "A platform independent file lock."
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||||
|
testing = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "diff-cover (>=7.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
@@ -84,14 +120,11 @@ python-versions = "*"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "21.3"
|
version = "23.1"
|
||||||
description = "Core utilities for Python packages"
|
description = "Core utilities for Python packages"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
|
||||||
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
@@ -103,15 +136,15 @@ python-versions = ">=3.7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
version = "2.5.2"
|
version = "3.7.0"
|
||||||
description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"]
|
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||||
test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"]
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pluggy"
|
name = "pluggy"
|
||||||
@@ -122,8 +155,8 @@ optional = false
|
|||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["pytest-benchmark", "pytest"]
|
dev = ["pre-commit", "tox"]
|
||||||
dev = ["tox", "pre-commit"]
|
testing = ["pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "py"
|
||||||
@@ -134,15 +167,20 @@ optional = false
|
|||||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyparsing"
|
name = "pyproject-api"
|
||||||
version = "3.0.9"
|
version = "1.5.2"
|
||||||
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
description = "API to interact with the python pyproject.toml based projects"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6.8"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
packaging = ">=23.1"
|
||||||
|
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
diagrams = ["railroad-diagrams", "jinja2"]
|
docs = ["furo (>=2023.5.20)", "sphinx-autodoc-typehints (>=1.23,!=1.23.4)", "sphinx (>=7.0.1)"]
|
||||||
|
testing = ["covdefaults (>=2.3)", "importlib-metadata (>=6.6)", "pytest-cov (>=4.1)", "pytest-mock (>=3.10)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "wheel (>=0.40)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
@@ -194,16 +232,61 @@ category = "main"
|
|||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tox"
|
||||||
|
version = "4.6.3"
|
||||||
|
description = "tox is a generic virtualenv management and test command line tool"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cachetools = ">=5.3.1"
|
||||||
|
chardet = ">=5.1"
|
||||||
|
colorama = ">=0.4.6"
|
||||||
|
filelock = ">=3.12.2"
|
||||||
|
packaging = ">=23.1"
|
||||||
|
platformdirs = ">=3.5.3"
|
||||||
|
pluggy = ">=1"
|
||||||
|
pyproject-api = ">=1.5.2"
|
||||||
|
tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""}
|
||||||
|
virtualenv = ">=20.23.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2023.5.20)", "sphinx-argparse-cli (>=1.11.1)", "sphinx-autodoc-typehints (>=1.23.2,!=1.23.4)", "sphinx-copybutton (>=0.5.2)", "sphinx-inline-tabs (>=2023.4.21)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||||
|
testing = ["build[virtualenv] (>=0.10)", "covdefaults (>=2.3)", "detect-test-pollution (>=1.1.1)", "devpi-process (>=0.3.1)", "diff-cover (>=7.6)", "distlib (>=0.3.6)", "flaky (>=3.7)", "hatch-vcs (>=0.3)", "hatchling (>=1.17.1)", "psutil (>=5.9.5)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)", "pytest-xdist (>=3.3.1)", "pytest (>=7.3.2)", "re-assert (>=1.1)", "time-machine (>=2.10)", "wheel (>=0.40)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "virtualenv"
|
||||||
|
version = "20.23.1"
|
||||||
|
description = "Virtual Python Environment builder"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
distlib = ">=0.3.6,<1"
|
||||||
|
filelock = ">=3.12,<4"
|
||||||
|
platformdirs = ">=3.5.1,<4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2023.5.20)", "proselint (>=0.13)", "sphinx-argparse (>=0.4)", "sphinx (>=7.0.1)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
|
||||||
|
test = ["covdefaults (>=2.3)", "coverage-enable-subprocess (>=1)", "coverage (>=7.2.7)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest-env (>=0.8.1)", "pytest-freezer (>=0.4.6)", "pytest-mock (>=3.10)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "pytest (>=7.3.1)", "setuptools (>=67.8)", "time-machine (>=2.9)"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
|
content-hash = "5d0edd070ea010edb4e2ade88dc37324b8b4b04f22db78e49db161185365849b"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
attrs = []
|
attrs = []
|
||||||
black = []
|
black = []
|
||||||
|
cachetools = []
|
||||||
|
chardet = []
|
||||||
click = []
|
click = []
|
||||||
colorama = []
|
colorama = []
|
||||||
|
distlib = []
|
||||||
|
filelock = []
|
||||||
iniconfig = []
|
iniconfig = []
|
||||||
isort = []
|
isort = []
|
||||||
mypy-extensions = []
|
mypy-extensions = []
|
||||||
@@ -212,8 +295,10 @@ pathspec = []
|
|||||||
platformdirs = []
|
platformdirs = []
|
||||||
pluggy = []
|
pluggy = []
|
||||||
py = []
|
py = []
|
||||||
pyparsing = []
|
pyproject-api = []
|
||||||
pytest = []
|
pytest = []
|
||||||
pytest-randomly = []
|
pytest-randomly = []
|
||||||
pytest-repeat = []
|
pytest-repeat = []
|
||||||
tomli = []
|
tomli = []
|
||||||
|
tox = []
|
||||||
|
virtualenv = []
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "vban-cmd"
|
name = "vban-cmd"
|
||||||
version = "1.8.1"
|
version = "2.4.3"
|
||||||
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
description = "Python interface for the VBAN RT Packet Service (Sendtext)"
|
||||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -18,11 +18,26 @@ pytest-randomly = "^3.12.0"
|
|||||||
pytest-repeat = "^0.9.1"
|
pytest-repeat = "^0.9.1"
|
||||||
black = "^22.3.0"
|
black = "^22.3.0"
|
||||||
isort = "^5.10.1"
|
isort = "^5.10.1"
|
||||||
|
tox = "^4.6.3"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[tool.poetry.scripts]
|
||||||
|
gui = "scripts:ex_gui"
|
||||||
obs = "scripts:ex_obs"
|
obs = "scripts:ex_obs"
|
||||||
observer = "scripts:ex_observer"
|
observer = "scripts:ex_observer"
|
||||||
|
test = "scripts:test"
|
||||||
|
|
||||||
|
[tool.tox]
|
||||||
|
legacy_tox_ini = """
|
||||||
|
[tox]
|
||||||
|
envlist = py310,py311
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
allowlist_externals = poetry
|
||||||
|
commands =
|
||||||
|
poetry install -v
|
||||||
|
poetry run pytest tests/
|
||||||
|
"""
|
||||||
|
|||||||
18
scripts.py
18
scripts.py
@@ -1,12 +1,22 @@
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def ex_gui():
|
||||||
|
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||||
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_obs():
|
def ex_obs():
|
||||||
path = Path.cwd() / "examples" / "obs" / "."
|
scriptpath = Path.cwd() / "examples" / "obs" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
def ex_observer():
|
def ex_observer():
|
||||||
path = Path.cwd() / "examples" / "observer" / "."
|
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
||||||
subprocess.run(["py", str(path)])
|
subprocess.run([sys.executable, str(scriptpath)])
|
||||||
|
|
||||||
|
|
||||||
|
def test():
|
||||||
|
subprocess.run(["tox"])
|
||||||
|
|||||||
@@ -3,23 +3,21 @@ import sys
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
import vban_cmd
|
import vban_cmd
|
||||||
from vban_cmd.kinds import KindId, kinds_all
|
from vban_cmd.kinds import KindId
|
||||||
from vban_cmd.kinds import request_kind_map as kindmap
|
from vban_cmd.kinds import request_kind_map as kindmap
|
||||||
|
|
||||||
# let's keep things random
|
# let's keep things random
|
||||||
kind_id = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
KIND_ID = random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||||
|
|
||||||
opts = {
|
opts = {
|
||||||
"ip": "ws.local",
|
"ip": "testing.local",
|
||||||
"streamname": "workstation",
|
"streamname": "testing",
|
||||||
"port": 6990,
|
"port": 6990,
|
||||||
"bps": 0,
|
"bps": 0,
|
||||||
"sync": True,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vbans = {kind.name: vban_cmd.api(kind.name, **opts) for kind in kinds_all}
|
vban = vban_cmd.api(KIND_ID, **opts)
|
||||||
tests = vbans[kind_id]
|
kind = kindmap(KIND_ID)
|
||||||
kind = kindmap(kind_id)
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -42,9 +40,9 @@ data = Data()
|
|||||||
|
|
||||||
def setup_module():
|
def setup_module():
|
||||||
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
print(f"\nRunning tests for kind [{data.name}]\n", file=sys.stdout)
|
||||||
tests.login()
|
vban.login()
|
||||||
tests.command.reset()
|
vban.command.reset()
|
||||||
|
|
||||||
|
|
||||||
def teardown_module():
|
def teardown_module():
|
||||||
tests.logout()
|
vban.logout()
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests import data, tests
|
from tests import data, vban
|
||||||
|
|
||||||
|
|
||||||
class TestSetAndGetBoolHigher:
|
class TestSetAndGetBoolHigher:
|
||||||
@@ -12,18 +10,18 @@ class TestSetAndGetBoolHigher:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup_class(cls):
|
def setup_class(cls):
|
||||||
tests.apply_config("example")
|
vban.apply_config("example")
|
||||||
|
|
||||||
def test_it_tests_config_string(self):
|
def test_it_tests_config_string(self):
|
||||||
assert "PhysStrip" in tests.strip[data.phys_in].label
|
assert "PhysStrip" in vban.strip[data.phys_in].label
|
||||||
assert "VirtStrip" in tests.strip[data.virt_in].label
|
assert "VirtStrip" in vban.strip[data.virt_in].label
|
||||||
|
|
||||||
def test_it_tests_config_bool(self):
|
def test_it_tests_config_bool(self):
|
||||||
assert tests.strip[0].A1 == True
|
assert vban.strip[0].A1 == True
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
"not config.getoption('--run-slow')",
|
"not config.getoption('--run-slow')",
|
||||||
reason="Only run when --run-slow is given",
|
reason="Only run when --run-slow is given",
|
||||||
)
|
)
|
||||||
def test_it_tests_config_busmode(self):
|
def test_it_tests_config_busmode(self):
|
||||||
assert tests.bus[data.phys_out].mode.get() == "composite"
|
assert vban.bus[data.phys_out].mode.get() == "composite"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests import data, tests
|
from tests import data, vban
|
||||||
|
|
||||||
|
|
||||||
class TestRemoteFactories:
|
class TestRemoteFactories:
|
||||||
@@ -11,33 +11,45 @@ class TestRemoteFactories:
|
|||||||
reason="Skip test if kind is not basic",
|
reason="Skip test if kind is not basic",
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_basic(self):
|
def test_it_tests_remote_attrs_for_basic(self):
|
||||||
assert hasattr(tests, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(tests, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(tests, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(tests.strip) == 3
|
assert len(vban.strip) == 3
|
||||||
assert len(tests.bus) == 2
|
assert len(vban.bus) == 2
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 6 and len(vban.vban.outstream) == 5
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "banana",
|
data.name != "banana",
|
||||||
reason="Skip test if kind is not basic",
|
reason="Skip test if kind is not basic",
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_banana(self):
|
def test_it_tests_remote_attrs_for_banana(self):
|
||||||
assert hasattr(tests, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(tests, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(tests, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(tests.strip) == 5
|
assert len(vban.strip) == 5
|
||||||
assert len(tests.bus) == 5
|
assert len(vban.bus) == 5
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
reason="Skip test if kind is not basic",
|
reason="Skip test if kind is not basic",
|
||||||
)
|
)
|
||||||
def test_it_tests_remote_attrs_for_potato(self):
|
def test_it_tests_remote_attrs_for_potato(self):
|
||||||
assert hasattr(tests, "strip")
|
assert hasattr(vban, "strip")
|
||||||
assert hasattr(tests, "bus")
|
assert hasattr(vban, "bus")
|
||||||
assert hasattr(tests, "command")
|
assert hasattr(vban, "command")
|
||||||
|
assert hasattr(vban, "button")
|
||||||
|
assert hasattr(vban, "vban")
|
||||||
|
|
||||||
assert len(tests.strip) == 8
|
assert len(vban.strip) == 8
|
||||||
assert len(tests.bus) == 8
|
assert len(vban.bus) == 8
|
||||||
|
assert len(vban.button) == 80
|
||||||
|
assert len(vban.vban.instream) == 10 and len(vban.vban.outstream) == 9
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from tests import data, tests
|
from tests import data, vban
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", [False, True])
|
@pytest.mark.parametrize("value", [False, True])
|
||||||
@@ -17,8 +17,8 @@ class TestSetAndGetBoolHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name == "banana",
|
data.name == "banana",
|
||||||
@@ -31,23 +31,22 @@ class TestSetAndGetBoolHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_bool_params_mc(self, index, param, value):
|
def test_it_sets_and_gets_strip_bool_params_mc(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,param",
|
"index,param",
|
||||||
[
|
[
|
||||||
(data.phys_out, "eq"),
|
|
||||||
(data.phys_out, "mute"),
|
(data.phys_out, "mute"),
|
||||||
(data.virt_out, "eq_ab"),
|
|
||||||
(data.virt_out, "sel"),
|
(data.virt_out, "sel"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_bus_bool_params(self, index, param, value):
|
||||||
setattr(tests.bus[index], param, value)
|
assert hasattr(vban.bus[index], param)
|
||||||
assert getattr(tests.bus[index], param) == value
|
setattr(vban.bus[index], param, value)
|
||||||
|
assert getattr(vban.bus[index], param) == value
|
||||||
|
|
||||||
""" bus modes tests, physical and virtual """
|
""" bus modes tests, physical and virtual """
|
||||||
|
|
||||||
@@ -66,8 +65,8 @@ class TestSetAndGetBoolHigher:
|
|||||||
# here it only makes sense to set/get bus modes as True
|
# here it only makes sense to set/get bus modes as True
|
||||||
if not value:
|
if not value:
|
||||||
value = True
|
value = True
|
||||||
setattr(tests.bus[index].mode, param, value)
|
setattr(vban.bus[index].mode, param, value)
|
||||||
assert getattr(tests.bus[index].mode, param) == value
|
assert getattr(vban.bus[index].mode, param) == value
|
||||||
|
|
||||||
""" command tests """
|
""" command tests """
|
||||||
|
|
||||||
@@ -76,7 +75,7 @@ class TestSetAndGetBoolHigher:
|
|||||||
[("lock")],
|
[("lock")],
|
||||||
)
|
)
|
||||||
def test_it_sets_command_bool_params(self, param, value):
|
def test_it_sets_command_bool_params(self, param, value):
|
||||||
setattr(tests.command, param, value)
|
setattr(vban.command, param, value)
|
||||||
|
|
||||||
|
|
||||||
class TestSetAndGetIntHigher:
|
class TestSetAndGetIntHigher:
|
||||||
@@ -94,8 +93,8 @@ class TestSetAndGetIntHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_bool_params(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
|
|
||||||
class TestSetAndGetFloatHigher:
|
class TestSetAndGetFloatHigher:
|
||||||
@@ -113,15 +112,15 @@ class TestSetAndGetFloatHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_float_params(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,value",
|
"index,value",
|
||||||
[(data.phys_in, 2), (data.phys_in, 2), (data.virt_in, 8), (data.virt_in, 8)],
|
[(data.phys_in, 2), (data.phys_in, 2), (data.virt_in, 8), (data.virt_in, 8)],
|
||||||
)
|
)
|
||||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||||
assert len(tests.strip[index].levels.prefader) == value
|
assert len(vban.strip[index].levels.prefader) == value
|
||||||
|
|
||||||
@pytest.mark.skipif(
|
@pytest.mark.skipif(
|
||||||
data.name != "potato",
|
data.name != "potato",
|
||||||
@@ -137,8 +136,42 @@ class TestSetAndGetFloatHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_gainlayer_values(self, index, j, value):
|
def test_it_sets_and_gets_strip_gainlayer_values(self, index, j, value):
|
||||||
tests.strip[index].gainlayer[j].gain = value
|
vban.strip[index].gainlayer[j].gain = value
|
||||||
assert tests.strip[index].gainlayer[j].gain == value
|
assert vban.strip[index].gainlayer[j].gain == value
|
||||||
|
|
||||||
|
""" strip tests, physical """
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
data.name != "potato",
|
||||||
|
reason="Only test if logged into Potato version",
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"index, param, value",
|
||||||
|
[
|
||||||
|
(data.phys_in, "gainin", -8.6),
|
||||||
|
(data.phys_in, "knee", 0.24),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_it_sets_strip_comp_params(self, index, param, value):
|
||||||
|
assert hasattr(vban.strip[index].comp, param)
|
||||||
|
setattr(vban.strip[index].comp, param, value)
|
||||||
|
# we can set but not get this value. Not in RT Packet.
|
||||||
|
|
||||||
|
@pytest.mark.skipif(
|
||||||
|
data.name != "potato",
|
||||||
|
reason="Only test if logged into Potato version",
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"index, param, value",
|
||||||
|
[
|
||||||
|
(data.phys_in, "bpsidechain", 120),
|
||||||
|
(data.phys_in, "hold", 3000),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_it_sets_and_gets_strip_gate_params(self, index, param, value):
|
||||||
|
assert hasattr(vban.strip[index].gate, param)
|
||||||
|
setattr(vban.strip[index].gate, param, value)
|
||||||
|
# we can set but not get this value. Not in RT Packet.
|
||||||
|
|
||||||
""" strip tests, virtual """
|
""" strip tests, virtual """
|
||||||
|
|
||||||
@@ -151,8 +184,8 @@ class TestSetAndGetFloatHigher:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_eq_params(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@@ -161,15 +194,15 @@ class TestSetAndGetFloatHigher:
|
|||||||
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
[(data.phys_out, "gain", -3.6), (data.virt_out, "gain", 5.8)],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_bus_float_params(self, index, param, value):
|
def test_it_sets_and_gets_bus_float_params(self, index, param, value):
|
||||||
setattr(tests.bus[index], param, value)
|
setattr(vban.bus[index], param, value)
|
||||||
assert getattr(tests.bus[index], param) == value
|
assert getattr(vban.bus[index], param) == value
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"index,value",
|
"index,value",
|
||||||
[(data.phys_out, 8), (data.virt_out, 8)],
|
[(data.phys_out, 8), (data.virt_out, 8)],
|
||||||
)
|
)
|
||||||
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
def test_it_gets_prefader_levels_and_compares_length_of_array(self, index, value):
|
||||||
assert len(tests.bus[index].levels.all) == value
|
assert len(vban.bus[index].levels.all) == value
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("value", ["test0", "test1"])
|
@pytest.mark.parametrize("value", ["test0", "test1"])
|
||||||
@@ -183,8 +216,8 @@ class TestSetAndGetStringHigher:
|
|||||||
[(data.phys_in, "label"), (data.virt_in, "label")],
|
[(data.phys_in, "label"), (data.virt_in, "label")],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
def test_it_sets_and_gets_strip_string_params(self, index, param, value):
|
||||||
setattr(tests.strip[index], param, value)
|
setattr(vban.strip[index], param, value)
|
||||||
assert getattr(tests.strip[index], param) == value
|
assert getattr(vban.strip[index], param) == value
|
||||||
|
|
||||||
""" bus tests, physical and virtual """
|
""" bus tests, physical and virtual """
|
||||||
|
|
||||||
@@ -193,5 +226,5 @@ class TestSetAndGetStringHigher:
|
|||||||
[(data.phys_out, "label"), (data.virt_out, "label")],
|
[(data.phys_out, "label"), (data.virt_out, "label")],
|
||||||
)
|
)
|
||||||
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
def test_it_sets_and_gets_bus_string_params(self, index, param, value):
|
||||||
setattr(tests.bus[index], param, value)
|
setattr(vban.bus[index], param, value)
|
||||||
assert getattr(tests.bus[index], param) == value
|
assert getattr(vban.bus[index], param) == value
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from vban_cmd import kinds
|
|
||||||
|
|
||||||
from tests import data, tests
|
from tests import data, vban
|
||||||
|
from vban_cmd import kinds
|
||||||
|
|
||||||
|
|
||||||
class TestPublicPacketLower:
|
class TestPublicPacketLower:
|
||||||
@@ -12,7 +12,7 @@ class TestPublicPacketLower:
|
|||||||
"""Tests for a valid rt data packet"""
|
"""Tests for a valid rt data packet"""
|
||||||
|
|
||||||
def test_it_gets_an_rt_data_packet(self):
|
def test_it_gets_an_rt_data_packet(self):
|
||||||
assert tests.public_packet.voicemeetertype in (
|
assert vban.public_packet.voicemeetertype in (
|
||||||
kind.name for kind in kinds.kinds_all
|
kind.name for kind in kinds.kinds_all
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ class TestSetRT:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_it_sends_a_text_request(self, kls, index, param, value):
|
def test_it_sends_a_text_request(self, kls, index, param, value):
|
||||||
tests._set_rt(f"{kls}[{index}]", param, value)
|
vban._set_rt(f"{kls}[{index}]", param, value)
|
||||||
time.sleep(0.02)
|
time.sleep(0.02)
|
||||||
target = getattr(tests, kls)[index]
|
target = getattr(vban, kls)[index]
|
||||||
assert getattr(target, param) == bool(value)
|
assert getattr(target, param) == bool(value)
|
||||||
|
|||||||
@@ -52,6 +52,23 @@ class Bus(IRemote):
|
|||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|
||||||
|
|
||||||
|
class BusEQ(IRemote):
|
||||||
|
@classmethod
|
||||||
|
def make(cls, remote, index):
|
||||||
|
BUSEQ_cls = type(
|
||||||
|
f"BusEQ{remote.kind}",
|
||||||
|
(cls,),
|
||||||
|
{
|
||||||
|
**{param: channel_bool_prop(param) for param in ["on", "ab"]},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return BUSEQ_cls(remote, index)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"Bus[{self.index}].eq"
|
||||||
|
|
||||||
|
|
||||||
class PhysicalBus(Bus):
|
class PhysicalBus(Bus):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f"{type(self).__name__}{self.index}"
|
||||||
@@ -85,7 +102,7 @@ class BusLevel(IRemote):
|
|||||||
def fget(i):
|
def fget(i):
|
||||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||||
|
|
||||||
if self._remote.running and self._remote.event.ldirty:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return tuple(
|
||||||
fget(i)
|
fget(i)
|
||||||
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
for i in self._remote.cache["bus_level"][self.range[0] : self.range[-1]]
|
||||||
@@ -167,11 +184,10 @@ def bus_factory(phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
|||||||
f"{BUS_cls.__name__}{remote.kind}",
|
f"{BUS_cls.__name__}{remote.kind}",
|
||||||
(BUS_cls,),
|
(BUS_cls,),
|
||||||
{
|
{
|
||||||
|
"eq": BusEQ.make(remote, i),
|
||||||
"levels": BusLevel(remote, i),
|
"levels": BusLevel(remote, i),
|
||||||
"mode": BUSMODEMIXIN_cls(remote, i),
|
"mode": BUSMODEMIXIN_cls(remote, i),
|
||||||
**{param: channel_bool_prop(param) for param in ["mute", "mono"]},
|
**{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(),
|
"label": channel_label_prop(),
|
||||||
},
|
},
|
||||||
)(remote, i)
|
)(remote, i)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .meta import action_prop
|
from .meta import action_fn
|
||||||
|
|
||||||
|
|
||||||
class Command(IRemote):
|
class Command(IRemote):
|
||||||
@@ -21,10 +21,9 @@ class Command(IRemote):
|
|||||||
(cls,),
|
(cls,),
|
||||||
{
|
{
|
||||||
**{
|
**{
|
||||||
param: action_prop(param)
|
param: action_fn(param) for param in ["show", "shutdown", "restart"]
|
||||||
for param in ["show", "shutdown", "restart"]
|
|
||||||
},
|
},
|
||||||
"hide": action_prop("show", val=0),
|
"hide": action_fn("show", val=0),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return CMD_cls(remote)
|
return CMD_cls(remote)
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import itertools
|
|||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .error import VBANCMDError
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import tomllib
|
import tomllib
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@@ -9,6 +11,8 @@ except ModuleNotFoundError:
|
|||||||
|
|
||||||
from .kinds import request_kind_map as kindmap
|
from .kinds import request_kind_map as kindmap
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TOMLStrBuilder:
|
class TOMLStrBuilder:
|
||||||
"""builds a config profile, as a string, for the toml parser"""
|
"""builds a config profile, as a string, for the toml parser"""
|
||||||
@@ -32,10 +36,18 @@ class TOMLStrBuilder:
|
|||||||
+ [f"B{i} = false" for i in range(1, self.kind.virt_out + 1)]
|
+ [f"B{i} = false" for i in range(1, self.kind.virt_out + 1)]
|
||||||
)
|
)
|
||||||
self.phys_strip_params = self.virt_strip_params + [
|
self.phys_strip_params = self.virt_strip_params + [
|
||||||
"comp = 0.0",
|
"comp.knob = 0.0",
|
||||||
"gate = 0.0",
|
"gate.knob = 0.0",
|
||||||
|
"denoiser.knob = 0.0",
|
||||||
|
"eq.on = false",
|
||||||
|
]
|
||||||
|
self.bus_float = ["gain = 0.0"]
|
||||||
|
self.bus_params = [
|
||||||
|
"mono = false",
|
||||||
|
"eq.on = false",
|
||||||
|
"mute = false",
|
||||||
|
"gain = 0.0",
|
||||||
]
|
]
|
||||||
self.bus_bool = ["mono = false", "eq = false", "mute = false"]
|
|
||||||
|
|
||||||
if profile == "reset":
|
if profile == "reset":
|
||||||
self.reset_config()
|
self.reset_config()
|
||||||
@@ -66,7 +78,7 @@ class TOMLStrBuilder:
|
|||||||
else self.virt_strip_params
|
else self.virt_strip_params
|
||||||
)
|
)
|
||||||
case "bus":
|
case "bus":
|
||||||
toml_str += ("\n").join(self.bus_bool)
|
toml_str += ("\n").join(self.bus_params)
|
||||||
case _:
|
case _:
|
||||||
pass
|
pass
|
||||||
return toml_str + "\n"
|
return toml_str + "\n"
|
||||||
@@ -119,10 +131,9 @@ class Loader(metaclass=SingletonType):
|
|||||||
loads data into memory if not found
|
loads data into memory if not found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger = logging.getLogger("config.Loader")
|
|
||||||
|
|
||||||
def __init__(self, kind):
|
def __init__(self, kind):
|
||||||
self._kind = kind
|
self._kind = kind
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self._configs = dict()
|
self._configs = dict()
|
||||||
self.defaults(kind)
|
self.defaults(kind)
|
||||||
self.parser = None
|
self.parser = None
|
||||||
@@ -166,16 +177,16 @@ def loader(kind):
|
|||||||
|
|
||||||
returns configs loaded into memory
|
returns configs loaded into memory
|
||||||
"""
|
"""
|
||||||
logger = logging.getLogger("config.loader")
|
logger_loader = logger.getChild("loader")
|
||||||
loader = Loader(kind)
|
loader = Loader(kind)
|
||||||
|
|
||||||
for path in (
|
for path in (
|
||||||
Path.cwd() / "configs" / kind.name,
|
Path.cwd() / "configs" / kind.name,
|
||||||
Path(__file__).parent / "configs" / kind.name,
|
Path.home() / ".config" / "vban-cmd" / kind.name,
|
||||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
Path.home() / "Documents" / "Voicemeeter" / "configs" / kind.name,
|
||||||
):
|
):
|
||||||
if path.is_dir():
|
if path.is_dir():
|
||||||
logger.info(f"Checking [{path}] for TOML config files:")
|
logger_loader.info(f"Checking [{path}] for TOML config files:")
|
||||||
for file in path.glob("*.toml"):
|
for file in path.glob("*.toml"):
|
||||||
identifier = file.with_suffix("").stem
|
identifier = file.with_suffix("").stem
|
||||||
if loader.parse(identifier, file):
|
if loader.parse(identifier, file):
|
||||||
@@ -191,6 +202,6 @@ def request_config(kind_id: str):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
configs = loader(kindmap(kind_id))
|
configs = loader(kindmap(kind_id))
|
||||||
except KeyError as e:
|
except KeyError:
|
||||||
print(f"Unknown Voicemeeter kind '{kind_id}'")
|
raise VBANCMDError(f"Unknown Voicemeeter kind {kind_id}")
|
||||||
return configs
|
return configs
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
class VBANCMDError(Exception):
|
class VBANCMDError(Exception):
|
||||||
"""general errors"""
|
"""Base VBANCMD Exception class. Raised when general errors occur"""
|
||||||
|
|
||||||
pass
|
|
||||||
|
class VBANCMDConnectionError(VBANCMDError):
|
||||||
|
"""Exception raised when connection/timeout errors occur"""
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
import logging
|
import logging
|
||||||
from typing import Iterable, Union
|
from typing import Iterable, Union
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Event:
|
class Event:
|
||||||
"""Keeps track of event subscriptions"""
|
"""Keeps track of event subscriptions"""
|
||||||
|
|
||||||
logger = logging.getLogger("event.event")
|
|
||||||
|
|
||||||
def __init__(self, subs: dict):
|
def __init__(self, subs: dict):
|
||||||
self.subs = subs
|
self.subs = subs
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
def info(self, msg=None):
|
def info(self, msg=None):
|
||||||
info = (f"{msg} events",) if msg else ()
|
info = (f"{msg} events",) if msg else ()
|
||||||
|
|||||||
@@ -2,16 +2,21 @@ import logging
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Iterable, NoReturn
|
from typing import Iterable
|
||||||
|
|
||||||
from .bus import request_bus_obj as bus
|
from .bus import request_bus_obj as bus
|
||||||
from .command import Command
|
from .command import Command
|
||||||
from .config import request_config as configs
|
from .config import request_config as configs
|
||||||
|
from .error import VBANCMDError
|
||||||
from .kinds import KindMapClass
|
from .kinds import KindMapClass
|
||||||
from .kinds import request_kind_map as kindmap
|
from .kinds import request_kind_map as kindmap
|
||||||
|
from .macrobutton import MacroButton
|
||||||
from .strip import request_strip_obj as strip
|
from .strip import request_strip_obj as strip
|
||||||
|
from .vban import request_vban_obj as vban
|
||||||
from .vbancmd import VbanCmd
|
from .vbancmd import VbanCmd
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class FactoryBuilder:
|
class FactoryBuilder:
|
||||||
"""
|
"""
|
||||||
@@ -20,8 +25,9 @@ class FactoryBuilder:
|
|||||||
Separates construction from representation.
|
Separates construction from representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
logger = logging.getLogger("vbancmd.factorybuilder")
|
BuilderProgress = IntEnum(
|
||||||
BuilderProgress = IntEnum("BuilderProgress", "strip bus command", start=0)
|
"BuilderProgress", "strip bus command macrobutton vban", start=0
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, factory, kind: KindMapClass):
|
def __init__(self, factory, kind: KindMapClass):
|
||||||
self._factory = factory
|
self._factory = factory
|
||||||
@@ -30,9 +36,12 @@ class FactoryBuilder:
|
|||||||
f"Finished building strips for {self._factory}",
|
f"Finished building strips for {self._factory}",
|
||||||
f"Finished building buses for {self._factory}",
|
f"Finished building buses for {self._factory}",
|
||||||
f"Finished building commands for {self._factory}",
|
f"Finished building commands for {self._factory}",
|
||||||
|
f"Finished building macrobuttons for {self._factory}",
|
||||||
|
f"Finished building vban in/out streams for {self._factory}",
|
||||||
)
|
)
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
def _pinfo(self, name: str) -> NoReturn:
|
def _pinfo(self, name: str) -> None:
|
||||||
"""prints progress status for each step"""
|
"""prints progress status for each step"""
|
||||||
name = name.split("_")[1]
|
name = name.split("_")[1]
|
||||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
||||||
@@ -55,14 +64,19 @@ class FactoryBuilder:
|
|||||||
self._factory.command = Command.make(self._factory)
|
self._factory.command = Command.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
def make_macrobutton(self):
|
||||||
|
self._factory.button = tuple(MacroButton(self._factory, i) for i in range(80))
|
||||||
|
return self
|
||||||
|
|
||||||
|
def make_vban(self):
|
||||||
|
self._factory.vban = vban(self._factory)
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
class FactoryBase(VbanCmd):
|
class FactoryBase(VbanCmd):
|
||||||
"""Base class for factories, subclasses VbanCmd."""
|
"""Base class for factories, subclasses VbanCmd."""
|
||||||
|
|
||||||
def __init__(self, kind_id: str, **kwargs):
|
def __init__(self, kind_id: str, **kwargs):
|
||||||
defaultsubs = {"pdirty": True, "ldirty": False}
|
|
||||||
if "subs" in kwargs:
|
|
||||||
defaultsubs = defaultsubs | kwargs.pop("subs")
|
|
||||||
defaultkwargs = {
|
defaultkwargs = {
|
||||||
"ip": None,
|
"ip": None,
|
||||||
"port": 6980,
|
"port": 6980,
|
||||||
@@ -70,9 +84,14 @@ class FactoryBase(VbanCmd):
|
|||||||
"bps": 0,
|
"bps": 0,
|
||||||
"channel": 0,
|
"channel": 0,
|
||||||
"ratelimit": 0.01,
|
"ratelimit": 0.01,
|
||||||
|
"timeout": 5,
|
||||||
|
"outbound": False,
|
||||||
"sync": False,
|
"sync": False,
|
||||||
"subs": defaultsubs,
|
"pdirty": False,
|
||||||
|
"ldirty": False,
|
||||||
}
|
}
|
||||||
|
if "subs" in kwargs:
|
||||||
|
defaultkwargs |= kwargs.pop("subs") # for backwards compatibility
|
||||||
kwargs = defaultkwargs | kwargs
|
kwargs = defaultkwargs | kwargs
|
||||||
self.kind = kindmap(kind_id)
|
self.kind = kindmap(kind_id)
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
@@ -81,12 +100,20 @@ class FactoryBase(VbanCmd):
|
|||||||
self.builder.make_strip,
|
self.builder.make_strip,
|
||||||
self.builder.make_bus,
|
self.builder.make_bus,
|
||||||
self.builder.make_command,
|
self.builder.make_command,
|
||||||
|
self.builder.make_macrobutton,
|
||||||
|
self.builder.make_vban,
|
||||||
)
|
)
|
||||||
self._configs = None
|
self._configs = None
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return f"Voicemeeter {self.kind}"
|
return f"Voicemeeter {self.kind}"
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return (
|
||||||
|
type(self).__name__
|
||||||
|
+ f"({self.kind}, ip='{self.ip}', port={self.port}, streamname='{self.streamname}')"
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def steps(self):
|
def steps(self):
|
||||||
@@ -188,9 +215,12 @@ def request_vbancmd_obj(kind_id: str, **kwargs) -> VbanCmd:
|
|||||||
|
|
||||||
Returns a reference to a VbanCmd class of a kind
|
Returns a reference to a VbanCmd class of a kind
|
||||||
"""
|
"""
|
||||||
|
logger_entry = logger.getChild("factory.request_vbancmd_obj")
|
||||||
|
|
||||||
VBANCMD_obj = None
|
VBANCMD_obj = None
|
||||||
try:
|
try:
|
||||||
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
VBANCMD_obj = vbancmd_factory(kind_id, **kwargs)
|
||||||
except (ValueError, TypeError) as e:
|
except (ValueError, TypeError) as e:
|
||||||
raise SystemExit(e)
|
logger_entry.exception(f"{type(e).__name__}: {e}")
|
||||||
|
raise VBANCMDError(str(e)) from e
|
||||||
return VBANCMD_obj
|
return VBANCMD_obj
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Modes:
|
class Modes:
|
||||||
@@ -26,9 +29,9 @@ class Modes:
|
|||||||
|
|
||||||
_mask: hex = 0x000000F0
|
_mask: hex = 0x000000F0
|
||||||
|
|
||||||
_eq_on: hex = 0x00000100
|
_on: hex = 0x00000100 # eq.on
|
||||||
_cross: hex = 0x00000200
|
_cross: hex = 0x00000200
|
||||||
_eq_ab: hex = 0x00000800
|
_ab: hex = 0x00000800 # eq.ab
|
||||||
|
|
||||||
_busa: hex = 0x00001000
|
_busa: hex = 0x00001000
|
||||||
_busa1: hex = 0x00001000
|
_busa1: hex = 0x00001000
|
||||||
@@ -85,10 +88,12 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
def __init__(self, remote, index=None):
|
def __init__(self, remote, index=None):
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self.index = index
|
self.index = index
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self._modes = Modes()
|
self._modes = Modes()
|
||||||
|
|
||||||
def getter(self, param):
|
def getter(self, param):
|
||||||
cmd = f"{self.identifier}.{param}"
|
cmd = self._cmd(param)
|
||||||
|
self.logger.debug(f"getter: {cmd}")
|
||||||
if cmd in self._remote.cache:
|
if cmd in self._remote.cache:
|
||||||
return self._remote.cache.pop(cmd)
|
return self._remote.cache.pop(cmd)
|
||||||
if self._remote.sync:
|
if self._remote.sync:
|
||||||
@@ -96,8 +101,16 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
|
|
||||||
def setter(self, param, val):
|
def setter(self, param, val):
|
||||||
"""Sends a string request RT packet."""
|
"""Sends a string request RT packet."""
|
||||||
self._remote._set_rt(f"{self.identifier}", param, val)
|
self.logger.debug(f"setter: {self._cmd(param)}={val}")
|
||||||
|
self._remote._set_rt(self._cmd(param), val)
|
||||||
|
|
||||||
|
def _cmd(self, param):
|
||||||
|
cmd = (self.identifier,)
|
||||||
|
if param:
|
||||||
|
cmd += (f".{param}",)
|
||||||
|
return "".join(cmd)
|
||||||
|
|
||||||
|
@property
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def identifier(self):
|
def identifier(self):
|
||||||
pass
|
pass
|
||||||
@@ -113,20 +126,26 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
def fget(attr, val):
|
def fget(attr, val):
|
||||||
if attr == "mode":
|
if attr == "mode":
|
||||||
return (f"mode.{val}", 1)
|
return (f"mode.{val}", 1)
|
||||||
|
elif attr == "knob":
|
||||||
|
return ("", val)
|
||||||
return (attr, val)
|
return (attr, val)
|
||||||
|
|
||||||
script = str()
|
|
||||||
for attr, val in data.items():
|
for attr, val in data.items():
|
||||||
if hasattr(self, attr):
|
if not isinstance(val, dict):
|
||||||
|
if attr in dir(self): # avoid calling getattr (with hasattr)
|
||||||
attr, val = fget(attr, val)
|
attr, val = fget(attr, val)
|
||||||
if isinstance(val, bool):
|
if isinstance(val, bool):
|
||||||
val = 1 if val else 0
|
val = 1 if val else 0
|
||||||
|
|
||||||
self._remote.cache[f"{self.identifier}.{attr}"] = val
|
self._remote.cache[self._cmd(attr)] = val
|
||||||
script += f"{self.identifier}.{attr}={val};"
|
self._remote._script += f"{self._cmd(attr)}={val};"
|
||||||
|
else:
|
||||||
|
target = getattr(self, attr)
|
||||||
|
target.apply(val)
|
||||||
|
|
||||||
self._remote.sendtext(script)
|
self._remote.sendtext(self._remote._script)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def then_wait(self):
|
def then_wait(self):
|
||||||
|
self._remote._script = str()
|
||||||
time.sleep(self._remote.DELAY)
|
time.sleep(self._remote.DELAY)
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import Enum, unique
|
from enum import Enum, unique
|
||||||
|
|
||||||
|
from .error import VBANCMDError
|
||||||
|
|
||||||
|
|
||||||
@unique
|
@unique
|
||||||
class KindId(Enum):
|
class KindId(Enum):
|
||||||
@@ -51,6 +53,14 @@ class KindMapClass(metaclass=SingletonType):
|
|||||||
def num_bus(self):
|
def num_bus(self):
|
||||||
return sum(self.outs)
|
return sum(self.outs)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_strip_levels(self) -> int:
|
||||||
|
return 2 * self.phys_in + 8 * self.virt_in
|
||||||
|
|
||||||
|
@property
|
||||||
|
def num_bus_levels(self) -> int:
|
||||||
|
return 8 * (self.phys_out + self.virt_out)
|
||||||
|
|
||||||
def __str__(self) -> str:
|
def __str__(self) -> str:
|
||||||
return self.name.capitalize()
|
return self.name.capitalize()
|
||||||
|
|
||||||
@@ -60,7 +70,7 @@ class BasicMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (2, 1)
|
ins: tuple = (2, 1)
|
||||||
outs: tuple = (1, 1)
|
outs: tuple = (1, 1)
|
||||||
vban: tuple = (4, 4)
|
vban: tuple = (4, 4, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -68,7 +78,7 @@ class BananaMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (3, 2)
|
ins: tuple = (3, 2)
|
||||||
outs: tuple = (3, 2)
|
outs: tuple = (3, 2)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@@ -76,7 +86,7 @@ class PotatoMap(KindMapClass):
|
|||||||
name: str
|
name: str
|
||||||
ins: tuple = (5, 3)
|
ins: tuple = (5, 3)
|
||||||
outs: tuple = (5, 3)
|
outs: tuple = (5, 3)
|
||||||
vban: tuple = (8, 8)
|
vban: tuple = (8, 8, 1, 1)
|
||||||
|
|
||||||
|
|
||||||
def kind_factory(kind_id):
|
def kind_factory(kind_id):
|
||||||
@@ -97,7 +107,7 @@ def request_kind_map(kind_id):
|
|||||||
try:
|
try:
|
||||||
KIND_obj = kind_factory(kind_id)
|
KIND_obj = kind_factory(kind_id)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(e)
|
raise VBANCMDError(str(e)) from e
|
||||||
return KIND_obj
|
return KIND_obj
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
36
vban_cmd/macrobutton.py
Normal file
36
vban_cmd/macrobutton.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
from .iremote import IRemote
|
||||||
|
|
||||||
|
|
||||||
|
class MacroButton(IRemote):
|
||||||
|
"""A placeholder class in case this interface is being used interchangeably with the Remote API"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self):
|
||||||
|
return f"command.button[{self.index}]"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self) -> bool:
|
||||||
|
self.logger.warning("button.state commands are not supported over VBAN")
|
||||||
|
|
||||||
|
@state.setter
|
||||||
|
def state(self, _):
|
||||||
|
self.logger.warning("button.state commands are not supported over VBAN")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def stateonly(self) -> bool:
|
||||||
|
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
||||||
|
|
||||||
|
@stateonly.setter
|
||||||
|
def stateonly(self, v):
|
||||||
|
self.logger.warning("button.stateonly commands are not supported over VBAN")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trigger(self) -> bool:
|
||||||
|
self.logger.warning("button.trigger commands are not supported over VBAN")
|
||||||
|
|
||||||
|
@trigger.setter
|
||||||
|
def trigger(self, _):
|
||||||
|
self.logger.warning("button.trigger commands are not supported over VBAN")
|
||||||
@@ -16,7 +16,7 @@ def channel_bool_prop(param):
|
|||||||
)[self.index],
|
)[self.index],
|
||||||
"little",
|
"little",
|
||||||
)
|
)
|
||||||
& getattr(self._modes, f'_{param.replace(".", "_").lower()}')
|
& getattr(self._modes, f"_{param.lower()}")
|
||||||
== 0
|
== 0
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,8 +91,8 @@ def bus_mode_prop(param):
|
|||||||
return property(fget, fset)
|
return property(fget, fset)
|
||||||
|
|
||||||
|
|
||||||
def action_prop(param, val=1):
|
def action_fn(param, val=1):
|
||||||
"""A param that performs an action"""
|
"""A function that performs an action"""
|
||||||
|
|
||||||
def fdo(self):
|
def fdo(self):
|
||||||
self.setter(param, val)
|
self.setter(param, val)
|
||||||
|
|||||||
@@ -1,23 +1,45 @@
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Generator
|
|
||||||
|
|
||||||
|
from .kinds import KindMapClass
|
||||||
from .util import comp
|
from .util import comp
|
||||||
|
|
||||||
|
VBAN_PROTOCOL_TXT = 0x40
|
||||||
|
VBAN_PROTOCOL_SERVICE = 0x60
|
||||||
|
|
||||||
VBAN_SERVICE_RTPACKETREGISTER = 32
|
VBAN_SERVICE_RTPACKETREGISTER = 32
|
||||||
VBAN_SERVICE_RTPACKET = 33
|
VBAN_SERVICE_RTPACKET = 33
|
||||||
|
|
||||||
MAX_PACKET_SIZE = 1436
|
MAX_PACKET_SIZE = 1436
|
||||||
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16 + 4
|
HEADER_SIZE = 4 + 1 + 1 + 1 + 1 + 16
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VbanRtPacket:
|
class VbanRtPacket:
|
||||||
"""Represents the body of a VBAN RT data packet"""
|
"""Represents the body of a VBAN RT data packet"""
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
_kind: KindMapClass
|
||||||
for k, v in kwargs.items():
|
_voicemeeterType: bytes # data[28:29]
|
||||||
setattr(self, k, v)
|
_reserved: bytes # data[29:30]
|
||||||
self._strip_level = self._generate_levels(self._inputLeveldB100)
|
_buffersize: bytes # data[30:32]
|
||||||
self._bus_level = self._generate_levels(self._outputLeveldB100)
|
_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:
|
def _generate_levels(self, levelarray) -> tuple:
|
||||||
return tuple(
|
return tuple(
|
||||||
@@ -25,6 +47,14 @@ class VbanRtPacket:
|
|||||||
for i in range(0, len(levelarray), 2)
|
for i in range(0, len(levelarray), 2)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def strip_levels(self):
|
||||||
|
return self._generate_levels(self._inputLeveldB100)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bus_levels(self):
|
||||||
|
return self._generate_levels(self._outputLeveldB100)
|
||||||
|
|
||||||
def pdirty(self, other) -> bool:
|
def pdirty(self, other) -> bool:
|
||||||
"""True iff any defined parameter has changed"""
|
"""True iff any defined parameter has changed"""
|
||||||
|
|
||||||
@@ -46,8 +76,8 @@ class VbanRtPacket:
|
|||||||
|
|
||||||
def ldirty(self, strip_cache, bus_cache) -> bool:
|
def ldirty(self, strip_cache, bus_cache) -> bool:
|
||||||
self._strip_comp, self._bus_comp = (
|
self._strip_comp, self._bus_comp = (
|
||||||
tuple(not val for val in comp(strip_cache, self._strip_level)),
|
tuple(not val for val in comp(strip_cache, self.strip_levels)),
|
||||||
tuple(not val for val in comp(bus_cache, self._bus_level)),
|
tuple(not val for val in comp(bus_cache, self.bus_levels)),
|
||||||
)
|
)
|
||||||
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
return any(any(l) for l in (self._strip_comp, self._bus_comp))
|
||||||
|
|
||||||
@@ -77,12 +107,12 @@ class VbanRtPacket:
|
|||||||
@property
|
@property
|
||||||
def inputlevels(self) -> tuple:
|
def inputlevels(self) -> tuple:
|
||||||
"""returns the entire level array across all inputs for a kind"""
|
"""returns the entire level array across all inputs for a kind"""
|
||||||
return self._strip_level[0 : (2 * self._kind.phys_in + 8 * self._kind.virt_in)]
|
return self.strip_levels[0 : self._kind.num_strip_levels]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def outputlevels(self) -> tuple:
|
def outputlevels(self) -> tuple:
|
||||||
"""returns the entire level array across all outputs for a kind"""
|
"""returns the entire level array across all outputs for a kind"""
|
||||||
return self._bus_level[0 : 8 * self._kind.num_bus]
|
return self.bus_levels[0 : self._kind.num_bus_levels]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def stripstate(self) -> tuple:
|
def stripstate(self) -> tuple:
|
||||||
@@ -180,13 +210,42 @@ class VbanRtPacket:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SubscribeHeader:
|
||||||
|
"""Represents the header an RT Packet Service subscription packet"""
|
||||||
|
|
||||||
|
name = "Register RTP"
|
||||||
|
timeout = 15
|
||||||
|
vban: bytes = "VBAN".encode()
|
||||||
|
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||||
|
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||||
|
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
||||||
|
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
||||||
|
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
||||||
|
framecounter: bytes = (0).to_bytes(4, "little")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def header(self):
|
||||||
|
header = self.vban
|
||||||
|
header += self.format_sr
|
||||||
|
header += self.format_nbs
|
||||||
|
header += self.format_nbc
|
||||||
|
header += self.format_bit
|
||||||
|
header += self.streamname
|
||||||
|
header += self.framecounter
|
||||||
|
assert (
|
||||||
|
len(header) == HEADER_SIZE + 4
|
||||||
|
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||||
|
return header
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VbanRtPacketHeader:
|
class VbanRtPacketHeader:
|
||||||
"""Represents the header of VBAN RT data packet"""
|
"""Represents the header of a VBAN RT response packet"""
|
||||||
|
|
||||||
name = "Voicemeeter-RTP"
|
name = "Voicemeeter-RTP"
|
||||||
vban: bytes = "VBAN".encode()
|
vban: bytes = "VBAN".encode()
|
||||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
format_sr: bytes = (VBAN_PROTOCOL_SERVICE).to_bytes(1, "little")
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
format_nbs: bytes = (0).to_bytes(1, "little")
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
format_nbc: bytes = (VBAN_SERVICE_RTPACKET).to_bytes(1, "little")
|
||||||
format_bit: bytes = (0).to_bytes(1, "little")
|
format_bit: bytes = (0).to_bytes(1, "little")
|
||||||
@@ -200,13 +259,13 @@ class VbanRtPacketHeader:
|
|||||||
header += self.format_nbc
|
header += self.format_nbc
|
||||||
header += self.format_bit
|
header += self.format_bit
|
||||||
header += self.streamname
|
header += self.streamname
|
||||||
assert len(header) == HEADER_SIZE - 4, f"Header expected {HEADER_SIZE-4} bytes"
|
assert len(header) == HEADER_SIZE, f"expected header size {HEADER_SIZE} bytes"
|
||||||
return header
|
return header
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class RequestHeader:
|
class RequestHeader:
|
||||||
"""Represents a REQUEST RT PACKET header"""
|
"""Represents the header of an REQUEST RT PACKET"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
bps_index: int
|
bps_index: int
|
||||||
@@ -218,7 +277,7 @@ class RequestHeader:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def sr(self):
|
def sr(self):
|
||||||
return (0x40 + self.bps_index).to_bytes(1, "little")
|
return (VBAN_PROTOCOL_TXT + self.bps_index).to_bytes(1, "little")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def nbc(self):
|
def nbc(self):
|
||||||
@@ -237,32 +296,7 @@ class RequestHeader:
|
|||||||
header += self.bit
|
header += self.bit
|
||||||
header += self.streamname
|
header += self.streamname
|
||||||
header += self.framecounter
|
header += self.framecounter
|
||||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
assert (
|
||||||
return header
|
len(header) == HEADER_SIZE + 4
|
||||||
|
), f"expected header size {HEADER_SIZE} bytes + 4 bytes framecounter ({HEADER_SIZE +4} bytes total)"
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class SubscribeHeader:
|
|
||||||
"""Represents a packet used to subscribe to the RT Packet Service"""
|
|
||||||
|
|
||||||
name = "Register RTP"
|
|
||||||
timeout = 15
|
|
||||||
vban: bytes = "VBAN".encode()
|
|
||||||
format_sr: bytes = (0x60).to_bytes(1, "little")
|
|
||||||
format_nbs: bytes = (0).to_bytes(1, "little")
|
|
||||||
format_nbc: bytes = (VBAN_SERVICE_RTPACKETREGISTER).to_bytes(1, "little")
|
|
||||||
format_bit: bytes = (timeout & 0x000000FF).to_bytes(1, "little") # timeout
|
|
||||||
streamname: bytes = name.encode("ascii") + bytes(16 - len(name))
|
|
||||||
framecounter: bytes = (0).to_bytes(4, "little")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def header(self):
|
|
||||||
header = self.vban
|
|
||||||
header += self.format_sr
|
|
||||||
header += self.format_nbs
|
|
||||||
header += self.format_nbc
|
|
||||||
header += self.format_bit
|
|
||||||
header += self.streamname
|
|
||||||
header += self.framecounter
|
|
||||||
assert len(header) == HEADER_SIZE, f"Header expected {HEADER_SIZE} bytes"
|
|
||||||
return header
|
return header
|
||||||
|
|||||||
@@ -51,25 +51,22 @@ class Strip(IRemote):
|
|||||||
|
|
||||||
|
|
||||||
class PhysicalStrip(Strip):
|
class PhysicalStrip(Strip):
|
||||||
|
@classmethod
|
||||||
|
def make(cls, remote, index):
|
||||||
|
return type(
|
||||||
|
f"PhysicalStrip{remote.kind}",
|
||||||
|
(cls,),
|
||||||
|
{
|
||||||
|
"comp": StripComp(remote, index),
|
||||||
|
"gate": StripGate(remote, index),
|
||||||
|
"denoiser": StripDenoiser(remote, index),
|
||||||
|
"eq": StripEQ(remote, index),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f"{type(self).__name__}{self.index}"
|
||||||
|
|
||||||
@property
|
|
||||||
def comp(self) -> float:
|
|
||||||
return
|
|
||||||
|
|
||||||
@comp.setter
|
|
||||||
def comp(self, val: float):
|
|
||||||
self.setter("Comp", val)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def gate(self) -> float:
|
|
||||||
return
|
|
||||||
|
|
||||||
@gate.setter
|
|
||||||
def gate(self, val: float):
|
|
||||||
self.setter("gate", val)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def device(self):
|
def device(self):
|
||||||
return
|
return
|
||||||
@@ -79,6 +76,182 @@ class PhysicalStrip(Strip):
|
|||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class StripComp(IRemote):
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"Strip[{self.index}].comp"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def knob(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@knob.setter
|
||||||
|
def knob(self, val: float):
|
||||||
|
self.setter("", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gainin(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@gainin.setter
|
||||||
|
def gainin(self, val: float):
|
||||||
|
self.setter("GainIn", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ratio(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@ratio.setter
|
||||||
|
def ratio(self, val: float):
|
||||||
|
self.setter("Ratio", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def threshold(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@threshold.setter
|
||||||
|
def threshold(self, val: float):
|
||||||
|
self.setter("Threshold", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@attack.setter
|
||||||
|
def attack(self, val: float):
|
||||||
|
self.setter("Attack", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@release.setter
|
||||||
|
def release(self, val: float):
|
||||||
|
self.setter("Release", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def knee(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@knee.setter
|
||||||
|
def knee(self, val: float):
|
||||||
|
self.setter("Knee", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def gainout(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@gainout.setter
|
||||||
|
def gainout(self, val: float):
|
||||||
|
self.setter("GainOut", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def makeup(self) -> bool:
|
||||||
|
return
|
||||||
|
|
||||||
|
@makeup.setter
|
||||||
|
def makeup(self, val: bool):
|
||||||
|
self.setter("makeup", 1 if val else 0)
|
||||||
|
|
||||||
|
|
||||||
|
class StripGate(IRemote):
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"Strip[{self.index}].gate"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def knob(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@knob.setter
|
||||||
|
def knob(self, val: float):
|
||||||
|
self.setter("", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def threshold(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@threshold.setter
|
||||||
|
def threshold(self, val: float):
|
||||||
|
self.setter("Threshold", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def damping(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@damping.setter
|
||||||
|
def damping(self, val: float):
|
||||||
|
self.setter("Damping", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bpsidechain(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@bpsidechain.setter
|
||||||
|
def bpsidechain(self, val: int):
|
||||||
|
self.setter("BPSidechain", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def attack(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@attack.setter
|
||||||
|
def attack(self, val: float):
|
||||||
|
self.setter("Attack", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hold(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@hold.setter
|
||||||
|
def hold(self, val: float):
|
||||||
|
self.setter("Hold", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def release(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@release.setter
|
||||||
|
def release(self, val: float):
|
||||||
|
self.setter("Release", val)
|
||||||
|
|
||||||
|
|
||||||
|
class StripDenoiser(IRemote):
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"Strip[{self.index}].denoiser"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def knob(self) -> float:
|
||||||
|
return
|
||||||
|
|
||||||
|
@knob.setter
|
||||||
|
def knob(self, val: float):
|
||||||
|
self.setter("", val)
|
||||||
|
|
||||||
|
|
||||||
|
class StripEQ(IRemote):
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"Strip[{self.index}].eq"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def on(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
@on.setter
|
||||||
|
def on(self, val: bool):
|
||||||
|
self.setter("on", 1 if val else 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ab(self):
|
||||||
|
return
|
||||||
|
|
||||||
|
@ab.setter
|
||||||
|
def ab(self, val: bool):
|
||||||
|
self.setter("ab", 1 if val else 0)
|
||||||
|
|
||||||
|
|
||||||
class VirtualStrip(Strip):
|
class VirtualStrip(Strip):
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f"{type(self).__name__}{self.index}"
|
||||||
@@ -123,7 +296,7 @@ class StripLevel(IRemote):
|
|||||||
def fget(i):
|
def fget(i):
|
||||||
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
return round((((1 << 16) - 1) - i) * -0.01, 1)
|
||||||
|
|
||||||
if self._remote.running and self._remote.event.ldirty:
|
if not self._remote.stopped() and self._remote.event.ldirty:
|
||||||
return tuple(
|
return tuple(
|
||||||
fget(i)
|
fget(i)
|
||||||
for i in self._remote.cache["strip_level"][
|
for i in self._remote.cache["strip_level"][
|
||||||
@@ -232,7 +405,7 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
|||||||
|
|
||||||
Returns a physical or virtual strip subclass
|
Returns a physical or virtual strip subclass
|
||||||
"""
|
"""
|
||||||
STRIP_cls = PhysicalStrip if is_phys_strip else VirtualStrip
|
STRIP_cls = PhysicalStrip.make(remote, i) if is_phys_strip else VirtualStrip
|
||||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||||
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
GAINLAYERMIXIN_cls = _make_gainlayer_mixin(remote, i)
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Subject:
|
class Subject:
|
||||||
"""Adds support for observers"""
|
|
||||||
|
|
||||||
logger = logging.getLogger("subject.subject")
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""list of current observers"""
|
"""Adds support for observers and callbacks"""
|
||||||
|
|
||||||
self._observers = list()
|
self._observers = list()
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def observers(self) -> list:
|
def observers(self) -> list:
|
||||||
@@ -17,38 +16,57 @@ class Subject:
|
|||||||
|
|
||||||
return self._observers
|
return self._observers
|
||||||
|
|
||||||
def notify(self, modifier=None):
|
def notify(self, event):
|
||||||
"""run callbacks on update"""
|
"""run callbacks on update"""
|
||||||
|
|
||||||
[o.on_update(modifier) for o in self._observers]
|
for o in self._observers:
|
||||||
|
if hasattr(o, "on_update"):
|
||||||
|
o.on_update(event)
|
||||||
|
else:
|
||||||
|
if o.__name__ == f"on_{event}":
|
||||||
|
o()
|
||||||
|
|
||||||
def add(self, observer):
|
def add(self, observer):
|
||||||
"""adds an observer to _observers"""
|
"""adds an observer to observers"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
iterator = iter(observer)
|
||||||
|
for o in iterator:
|
||||||
|
if o not in self._observers:
|
||||||
|
self._observers.append(o)
|
||||||
|
self.logger.info(f"{o} added to event observers")
|
||||||
|
else:
|
||||||
|
self.logger.error(f"Failed to add {o} to event observers")
|
||||||
|
except TypeError:
|
||||||
if observer not in self._observers:
|
if observer not in self._observers:
|
||||||
self._observers.append(observer)
|
self._observers.append(observer)
|
||||||
self.logger.info(f"{type(observer).__name__} added to event observers")
|
self.logger.info(f"{observer} added to event observers")
|
||||||
else:
|
else:
|
||||||
self.logger.error(
|
self.logger.error(f"Failed to add {observer} to event observers")
|
||||||
f"Failed to add {type(observer).__name__} to event observers"
|
|
||||||
)
|
|
||||||
|
|
||||||
register = add
|
register = add
|
||||||
|
|
||||||
def remove(self, observer):
|
def remove(self, observer):
|
||||||
"""removes an observer from _observers"""
|
"""removes an observer from observers"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._observers.remove(observer)
|
iterator = iter(observer)
|
||||||
self.logger.info(f"{type(observer).__name__} removed from event observers")
|
for o in iterator:
|
||||||
|
try:
|
||||||
|
self._observers.remove(o)
|
||||||
|
self.logger.info(f"{o} removed from event observers")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.logger.error(
|
self.logger.error(f"Failed to remove {o} from event observers")
|
||||||
f"Failed to remove {type(observer).__name__} from event observers"
|
except TypeError:
|
||||||
)
|
try:
|
||||||
|
self._observers.remove(observer)
|
||||||
|
self.logger.info(f"{observer} removed from event observers")
|
||||||
|
except ValueError:
|
||||||
|
self.logger.error(f"Failed to remove {observer} from event observers")
|
||||||
|
|
||||||
deregister = remove
|
deregister = remove
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""clears the _observers list"""
|
"""clears the observers list"""
|
||||||
|
|
||||||
self._observers.clear()
|
self._observers.clear()
|
||||||
|
|||||||
@@ -73,4 +73,18 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
|
|||||||
yield True
|
yield True
|
||||||
|
|
||||||
|
|
||||||
|
def deep_merge(dict1, dict2):
|
||||||
|
"""Generator function for deep merging two dicts"""
|
||||||
|
for k in set(dict1) | set(dict2):
|
||||||
|
if k in dict1 and k in dict2:
|
||||||
|
if isinstance(dict1[k], dict) and isinstance(dict2[k], dict):
|
||||||
|
yield k, dict(deep_merge(dict1[k], dict2[k]))
|
||||||
|
else:
|
||||||
|
yield k, dict2[k]
|
||||||
|
elif k in dict1:
|
||||||
|
yield k, dict1[k]
|
||||||
|
else:
|
||||||
|
yield k, dict2[k]
|
||||||
|
|
||||||
|
|
||||||
Socket = IntEnum("Socket", "register request response", start=0)
|
Socket = IntEnum("Socket", "register request response", start=0)
|
||||||
|
|||||||
242
vban_cmd/vban.py
Normal file
242
vban_cmd/vban.py
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
|
||||||
|
from .iremote import IRemote
|
||||||
|
from .kinds import kinds_all
|
||||||
|
|
||||||
|
|
||||||
|
class VbanStream(IRemote):
|
||||||
|
"""
|
||||||
|
Implements the common interface
|
||||||
|
|
||||||
|
Defines concrete implementation for vban stream
|
||||||
|
"""
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def __str__(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def identifier(self) -> str:
|
||||||
|
return f"vban.{self.direction}stream[{self.index}]"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def on(self) -> bool:
|
||||||
|
return
|
||||||
|
|
||||||
|
@on.setter
|
||||||
|
def on(self, val: bool):
|
||||||
|
self.setter("on", 1 if val else 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return
|
||||||
|
|
||||||
|
@name.setter
|
||||||
|
def name(self, val: str):
|
||||||
|
self.setter("name", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ip(self) -> str:
|
||||||
|
return
|
||||||
|
|
||||||
|
@ip.setter
|
||||||
|
def ip(self, val: str):
|
||||||
|
self.setter("ip", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def port(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@port.setter
|
||||||
|
def port(self, val: int):
|
||||||
|
if not 1024 <= val <= 65535:
|
||||||
|
self.logger.warning(
|
||||||
|
f"port got: {val} but expected a value from 1024 to 65535"
|
||||||
|
)
|
||||||
|
self.setter("port", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sr(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@sr.setter
|
||||||
|
def sr(self, val: int):
|
||||||
|
opts = (11025, 16000, 22050, 24000, 32000, 44100, 48000, 64000, 88200, 96000)
|
||||||
|
if val not in opts:
|
||||||
|
self.logger.warning(f"sr got: {val} but expected a value in {opts}")
|
||||||
|
self.setter("sr", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@channel.setter
|
||||||
|
def channel(self, val: int):
|
||||||
|
if not 1 <= val <= 8:
|
||||||
|
self.logger.warning(f"channel got: {val} but expected a value from 1 to 8")
|
||||||
|
self.setter("channel", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bit(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@bit.setter
|
||||||
|
def bit(self, val: int):
|
||||||
|
if val not in (16, 24):
|
||||||
|
self.logger.warning(f"bit got: {val} but expected value 16 or 24")
|
||||||
|
self.setter("bit", 1 if (val == 16) else 2)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def quality(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@quality.setter
|
||||||
|
def quality(self, val: int):
|
||||||
|
if not 0 <= val <= 4:
|
||||||
|
self.logger.warning(f"quality got: {val} but expected a value from 0 to 4")
|
||||||
|
self.setter("quality", val)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def route(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@route.setter
|
||||||
|
def route(self, val: int):
|
||||||
|
if not 0 <= val <= 8:
|
||||||
|
self.logger.warning(f"route got: {val} but expected a value from 0 to 8")
|
||||||
|
self.setter("route", val)
|
||||||
|
|
||||||
|
|
||||||
|
class VbanInstream(VbanStream):
|
||||||
|
"""
|
||||||
|
class representing a vban instream
|
||||||
|
|
||||||
|
subclasses VbanStream
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def direction(self) -> str:
|
||||||
|
return "in"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sr(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def channel(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
@property
|
||||||
|
def bit(self) -> int:
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
class VbanAudioInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Audio Instream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanMidiInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Midi Instream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanTextInstream(VbanInstream):
|
||||||
|
"""Represents a VBAN Text Instream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanOutstream(VbanStream):
|
||||||
|
"""
|
||||||
|
class representing a vban outstream
|
||||||
|
|
||||||
|
Subclasses VbanStream
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"{type(self).__name__}{self._remote.kind}{self.index}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def direction(self) -> str:
|
||||||
|
return "out"
|
||||||
|
|
||||||
|
|
||||||
|
class VbanAudioOutstream(VbanOutstream):
|
||||||
|
"""Represents a VBAN Audio Outstream"""
|
||||||
|
|
||||||
|
|
||||||
|
class VbanMidiOutstream(VbanOutstream):
|
||||||
|
"""Represents a VBAN Midi Outstream"""
|
||||||
|
|
||||||
|
|
||||||
|
def _make_stream_pair(remote, kind):
|
||||||
|
num_instream, num_outstream, num_midi, num_text = kind.vban
|
||||||
|
|
||||||
|
def _generate_streams(i, dir):
|
||||||
|
"""generator function for creating instream/outstream tuples"""
|
||||||
|
if dir == "in":
|
||||||
|
if i < num_instream:
|
||||||
|
yield VbanAudioInstream
|
||||||
|
elif i < num_instream + num_midi:
|
||||||
|
yield VbanMidiInstream
|
||||||
|
else:
|
||||||
|
yield VbanTextInstream
|
||||||
|
else:
|
||||||
|
if i < num_outstream:
|
||||||
|
yield VbanAudioOutstream
|
||||||
|
else:
|
||||||
|
yield VbanMidiOutstream
|
||||||
|
|
||||||
|
return (
|
||||||
|
tuple(
|
||||||
|
cls(remote, i)
|
||||||
|
for i in range(num_instream + num_midi + num_text)
|
||||||
|
for cls in _generate_streams(i, "in")
|
||||||
|
),
|
||||||
|
tuple(
|
||||||
|
cls(remote, i)
|
||||||
|
for i in range(num_outstream + num_midi)
|
||||||
|
for cls in _generate_streams(i, "out")
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _make_stream_pairs(remote):
|
||||||
|
return {kind.name: _make_stream_pair(remote, kind) for kind in kinds_all}
|
||||||
|
|
||||||
|
|
||||||
|
class Vban:
|
||||||
|
"""
|
||||||
|
class representing the vban module
|
||||||
|
|
||||||
|
Contains two tuples, one for each stream type
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, remote):
|
||||||
|
self.remote = remote
|
||||||
|
self.instream, self.outstream = _make_stream_pairs(remote)[remote.kind.name]
|
||||||
|
|
||||||
|
def enable(self):
|
||||||
|
"""if VBAN disabled there can be no communication with it"""
|
||||||
|
|
||||||
|
def disable(self):
|
||||||
|
self.remote._set_rt("vban.Enable", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def vban_factory(remote) -> Vban:
|
||||||
|
"""
|
||||||
|
Factory method for vban
|
||||||
|
|
||||||
|
Returns a class that represents the VBAN module.
|
||||||
|
"""
|
||||||
|
VBAN_cls = Vban
|
||||||
|
return type(f"{VBAN_cls.__name__}", (VBAN_cls,), {})(remote)
|
||||||
|
|
||||||
|
|
||||||
|
def request_vban_obj(remote) -> Vban:
|
||||||
|
"""
|
||||||
|
Vban entry point.
|
||||||
|
|
||||||
|
Returns a reference to a Vban class of a kind
|
||||||
|
"""
|
||||||
|
return vban_factory(remote)
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import logging
|
import logging
|
||||||
import socket
|
import socket
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Iterable, Optional, Union
|
from queue import Queue
|
||||||
|
from typing import Iterable, Union
|
||||||
try:
|
|
||||||
import tomllib
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
import tomli as tomllib
|
|
||||||
|
|
||||||
|
from .error import VBANCMDError
|
||||||
from .event import Event
|
from .event import Event
|
||||||
from .packet import RequestHeader
|
from .packet import RequestHeader
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
from .util import Socket, script
|
from .util import Socket, deep_merge, script
|
||||||
from .worker import Subscriber, Updater
|
from .worker import Producer, Subscriber, Updater
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class VbanCmd(metaclass=ABCMeta):
|
class VbanCmd(metaclass=ABCMeta):
|
||||||
@@ -28,15 +28,14 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
1000000, 1500000, 2000000, 3000000,
|
1000000, 1500000, 2000000, 3000000,
|
||||||
]
|
]
|
||||||
# fmt: on
|
# fmt: on
|
||||||
logger = logging.getLogger("vbancmd.vbancmd")
|
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
self.event = Event({k: kwargs.pop(k) for k in ("pdirty", "ldirty")})
|
||||||
|
if not kwargs["ip"]:
|
||||||
|
kwargs |= self._conn_from_toml()
|
||||||
for attr, val in kwargs.items():
|
for attr, val in kwargs.items():
|
||||||
setattr(self, attr, val)
|
setattr(self, attr, val)
|
||||||
if self.ip is None:
|
|
||||||
conn = self._conn_from_toml()
|
|
||||||
for attr, val in conn.items():
|
|
||||||
setattr(self, attr, val)
|
|
||||||
|
|
||||||
self.packet_request = RequestHeader(
|
self.packet_request = RequestHeader(
|
||||||
name=self.streamname,
|
name=self.streamname,
|
||||||
@@ -46,61 +45,94 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
self.socks = tuple(
|
self.socks = tuple(
|
||||||
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
socket.socket(socket.AF_INET, socket.SOCK_DGRAM) for _ in Socket
|
||||||
)
|
)
|
||||||
self.subject = Subject()
|
self.subject = self.observer = Subject()
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.event = Event(self.subs)
|
|
||||||
self._pdirty = False
|
self._pdirty = False
|
||||||
self._ldirty = False
|
self._ldirty = False
|
||||||
|
self._script = str()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
"""Ensure subclasses override str magic method"""
|
"""Ensure subclasses override str magic method"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _conn_from_toml(self) -> str:
|
def _conn_from_toml(self) -> dict:
|
||||||
filepath = Path.cwd() / "vban.toml"
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
|
def get_filepath():
|
||||||
|
filepaths = [
|
||||||
|
Path.cwd() / "vban.toml",
|
||||||
|
Path.cwd() / "configs" / "vban.toml",
|
||||||
|
Path.home() / ".config" / "vban-cmd" / "vban.toml",
|
||||||
|
Path.home() / "Documents" / "Voicemeeter" / "configs" / "vban.toml",
|
||||||
|
]
|
||||||
|
for filepath in filepaths:
|
||||||
|
if filepath.exists():
|
||||||
|
return filepath
|
||||||
|
|
||||||
|
if filepath := get_filepath():
|
||||||
with open(filepath, "rb") as f:
|
with open(filepath, "rb") as f:
|
||||||
conn = tomllib.load(f)
|
conn = tomllib.load(f)
|
||||||
|
assert (
|
||||||
|
"connection" in conn and "ip" in conn["connection"]
|
||||||
|
), "expected [connection][ip] in vban config"
|
||||||
return conn["connection"]
|
return conn["connection"]
|
||||||
|
raise VBANCMDError("no ip provided and no vban.toml located.")
|
||||||
|
|
||||||
def __enter__(self):
|
def __enter__(self):
|
||||||
self.login()
|
self.login()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def login(self):
|
def login(self) -> None:
|
||||||
"""Starts the subscriber and updater threads"""
|
"""Starts the subscriber and updater threads (unless in outbound mode)"""
|
||||||
self.running = True
|
if not self.outbound:
|
||||||
self.event.info()
|
self.event.info()
|
||||||
|
|
||||||
self.subscriber = Subscriber(self)
|
self.stop_event = threading.Event()
|
||||||
|
self.stop_event.clear()
|
||||||
|
self.subscriber = Subscriber(self, self.stop_event)
|
||||||
self.subscriber.start()
|
self.subscriber.start()
|
||||||
|
|
||||||
self.updater = Updater(self)
|
queue = Queue()
|
||||||
|
self.updater = Updater(self, queue)
|
||||||
self.updater.start()
|
self.updater.start()
|
||||||
|
self.producer = Producer(self, queue, self.stop_event)
|
||||||
|
self.producer.start()
|
||||||
|
|
||||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
self.logger.info(
|
||||||
|
"Successfully logged into VBANCMD {kind} with ip='{ip}', port={port}, streamname='{streamname}'".format(
|
||||||
|
**self.__dict__
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _set_rt(
|
def stopped(self):
|
||||||
self,
|
return self.stop_event.is_set()
|
||||||
id_: str,
|
|
||||||
param: Optional[str] = None,
|
def _set_rt(self, cmd: str, val: Union[str, float]):
|
||||||
val: Optional[Union[int, float]] = None,
|
|
||||||
):
|
|
||||||
"""Sends a string request command over a network."""
|
"""Sends a string request command over a network."""
|
||||||
cmd = id_ if not param else f"{id_}.{param}={val};"
|
|
||||||
self.socks[Socket.request].sendto(
|
self.socks[Socket.request].sendto(
|
||||||
self.packet_request.header + cmd.encode(),
|
self.packet_request.header + f"{cmd}={val};".encode(),
|
||||||
(socket.gethostbyname(self.ip), self.port),
|
(socket.gethostbyname(self.ip), self.port),
|
||||||
)
|
)
|
||||||
count = int.from_bytes(self.packet_request.framecounter, "little") + 1
|
self.packet_request.framecounter = (
|
||||||
self.packet_request.framecounter = count.to_bytes(4, "little")
|
int.from_bytes(self.packet_request.framecounter, "little") + 1
|
||||||
if param:
|
).to_bytes(4, "little")
|
||||||
self.cache[f"{id_}.{param}"] = val
|
self.cache[cmd] = val
|
||||||
|
|
||||||
@script
|
@script
|
||||||
def sendtext(self, cmd):
|
def sendtext(self, script):
|
||||||
"""Sends a multiple parameter string over a network."""
|
"""Sends a multiple parameter string over a network."""
|
||||||
self._set_rt(cmd)
|
self.socks[Socket.request].sendto(
|
||||||
|
self.packet_request.header + script.encode(),
|
||||||
|
(socket.gethostbyname(self.ip), self.port),
|
||||||
|
)
|
||||||
|
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)
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -127,7 +159,7 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
def public_packet(self):
|
def public_packet(self):
|
||||||
return self._public_packet
|
return self._public_packet
|
||||||
|
|
||||||
def clear_dirty(self):
|
def clear_dirty(self) -> None:
|
||||||
while self.pdirty:
|
while self.pdirty:
|
||||||
time.sleep(self.DELAY)
|
time.sleep(self.DELAY)
|
||||||
|
|
||||||
@@ -152,30 +184,46 @@ class VbanCmd(metaclass=ABCMeta):
|
|||||||
def param(key):
|
def param(key):
|
||||||
obj, m2, *rem = key.split("-")
|
obj, m2, *rem = key.split("-")
|
||||||
index = int(m2) if m2.isnumeric() else int(*rem)
|
index = int(m2) if m2.isnumeric() else int(*rem)
|
||||||
if obj in ("strip", "bus"):
|
if obj in ("strip", "bus", "button"):
|
||||||
return getattr(self, obj)[index]
|
return getattr(self, obj)[index]
|
||||||
else:
|
elif obj == "vban":
|
||||||
|
return getattr(getattr(self, obj), f"{m2}stream")[index]
|
||||||
raise ValueError(obj)
|
raise ValueError(obj)
|
||||||
|
|
||||||
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
[param(key).apply(datum).then_wait() for key, datum in data.items()]
|
||||||
|
|
||||||
def apply_config(self, name):
|
def apply_config(self, name):
|
||||||
"""applies a config from memory"""
|
"""applies a config from memory"""
|
||||||
error_msg = (
|
ERR_MSG = (
|
||||||
f"No config with name '{name}' is loaded into memory",
|
f"No config with name '{name}' is loaded into memory",
|
||||||
f"Known configs: {list(self.configs.keys())}",
|
f"Known configs: {list(self.configs.keys())}",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.apply(self.configs[name])
|
config = self.configs[name]
|
||||||
self.logger.info(f"Profile '{name}' applied!")
|
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
self.logger.error(("\n").join(error_msg))
|
self.logger.error(("\n").join(ERR_MSG))
|
||||||
|
raise VBANCMDError(("\n").join(ERR_MSG)) from e
|
||||||
|
|
||||||
def logout(self):
|
if "extends" in config:
|
||||||
self.running = False
|
extended = config["extends"]
|
||||||
time.sleep(0.2)
|
config = {
|
||||||
|
k: v
|
||||||
|
for k, v in deep_merge(self.configs[extended], config)
|
||||||
|
if k not in ("extends")
|
||||||
|
}
|
||||||
|
self.logger.debug(
|
||||||
|
f"profile '{name}' extends '{extended}', profiles merged.."
|
||||||
|
)
|
||||||
|
self.apply(config)
|
||||||
|
self.logger.info(f"Profile '{name}' applied!")
|
||||||
|
|
||||||
|
def logout(self) -> None:
|
||||||
|
if not self.stopped():
|
||||||
|
self.logger.debug("events thread shutdown started")
|
||||||
|
self.stop_event.set()
|
||||||
|
self.subscriber.join() # wait for subscriber thread to complete cycle
|
||||||
[sock.close() for sock in self.socks]
|
[sock.close() for sock in self.socks]
|
||||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||||
|
|
||||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
|
||||||
self.logout()
|
self.logout()
|
||||||
|
|||||||
@@ -4,69 +4,90 @@ import threading
|
|||||||
import time
|
import time
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from .error import VBANCMDError
|
from .error import VBANCMDConnectionError
|
||||||
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
from .packet import HEADER_SIZE, SubscribeHeader, VbanRtPacket, VbanRtPacketHeader
|
||||||
from .util import Socket, comp
|
from .util import Socket
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class Subscriber(threading.Thread):
|
class Subscriber(threading.Thread):
|
||||||
"""fire a subscription packet every 10 seconds"""
|
"""fire a subscription packet every 10 seconds"""
|
||||||
|
|
||||||
def __init__(self, remote):
|
def __init__(self, remote, stop_event):
|
||||||
super().__init__(name="subscriber", target=self.subscribe, daemon=True)
|
super().__init__(name="subscriber", daemon=False)
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
|
self.stop_event = stop_event
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
self.packet = SubscribeHeader()
|
self.packet = SubscribeHeader()
|
||||||
|
|
||||||
def subscribe(self):
|
def run(self):
|
||||||
while self._remote.running:
|
while not self.stopped():
|
||||||
try:
|
try:
|
||||||
self._remote.socks[Socket.register].sendto(
|
self._remote.socks[Socket.register].sendto(
|
||||||
self.packet.header,
|
self.packet.header,
|
||||||
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
(socket.gethostbyname(self._remote.ip), self._remote.port),
|
||||||
)
|
)
|
||||||
count = int.from_bytes(self.packet.framecounter, "little") + 1
|
self.packet.framecounter = (
|
||||||
self.packet.framecounter = count.to_bytes(4, "little")
|
int.from_bytes(self.packet.framecounter, "little") + 1
|
||||||
time.sleep(10)
|
).to_bytes(4, "little")
|
||||||
except socket.gaierror:
|
self.wait_until_stopped(10)
|
||||||
err_msg = f"Unable to resolve hostname {self._remote.ip}"
|
except socket.gaierror as e:
|
||||||
print(err_msg)
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
raise VBANCMDError(err_msg)
|
raise VBANCMDConnectionError(
|
||||||
|
f"unable to resolve hostname {self._remote.ip}"
|
||||||
|
) from e
|
||||||
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
|
|
||||||
|
def stopped(self):
|
||||||
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
|
def wait_until_stopped(self, timeout, period=0.2):
|
||||||
|
must_end = time.time() + timeout
|
||||||
|
while time.time() < must_end:
|
||||||
|
if self.stopped():
|
||||||
|
break
|
||||||
|
time.sleep(period)
|
||||||
|
|
||||||
|
|
||||||
class Updater(threading.Thread):
|
class Producer(threading.Thread):
|
||||||
"""
|
"""Continously send job queue to the Updater thread at a rate of self._remote.ratelimit."""
|
||||||
continously updates the public packet
|
|
||||||
|
|
||||||
notifies observers of event updates
|
def __init__(self, remote, queue, stop_event):
|
||||||
"""
|
super().__init__(name="producer", daemon=False)
|
||||||
|
|
||||||
logger = logging.getLogger("worker.updater")
|
|
||||||
|
|
||||||
def __init__(self, remote):
|
|
||||||
super().__init__(name="updater", target=self.update, daemon=True)
|
|
||||||
self._remote = remote
|
self._remote = remote
|
||||||
self._remote.socks[Socket.response].settimeout(5)
|
self.queue = queue
|
||||||
|
self.stop_event = stop_event
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
self.packet_expected = VbanRtPacketHeader()
|
||||||
|
self._remote.socks[Socket.response].settimeout(self._remote.timeout)
|
||||||
self._remote.socks[Socket.response].bind(
|
self._remote.socks[Socket.response].bind(
|
||||||
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
(socket.gethostbyname(socket.gethostname()), self._remote.port)
|
||||||
)
|
)
|
||||||
self.packet_expected = VbanRtPacketHeader()
|
|
||||||
self._remote._public_packet = self._get_rt()
|
self._remote._public_packet = self._get_rt()
|
||||||
(
|
(
|
||||||
self._remote.cache["strip_level"],
|
self._remote.cache["strip_level"],
|
||||||
self._remote.cache["bus_level"],
|
self._remote.cache["bus_level"],
|
||||||
) = self._remote._get_levels(self._remote.public_packet)
|
) = self._remote._get_levels(self._remote.public_packet)
|
||||||
p_in, v_in = self._remote.kind.ins
|
|
||||||
self._remote._strip_comp = [False] * (2 * p_in + 8 * v_in)
|
def _get_rt(self) -> VbanRtPacket:
|
||||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
"""Attempt to fetch data packet until a valid one found"""
|
||||||
|
|
||||||
|
def fget():
|
||||||
|
data = None
|
||||||
|
while not data:
|
||||||
|
data = self._fetch_rt_packet()
|
||||||
|
return data
|
||||||
|
|
||||||
|
return fget()
|
||||||
|
|
||||||
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
def _fetch_rt_packet(self) -> Optional[VbanRtPacket]:
|
||||||
try:
|
try:
|
||||||
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
data, _ = self._remote.socks[Socket.response].recvfrom(2048)
|
||||||
# check for packet data
|
# do we have packet data?
|
||||||
if len(data) > HEADER_SIZE:
|
if len(data) > HEADER_SIZE:
|
||||||
# check if packet is of type rt packet response
|
# is the packet of type VBAN RT response?
|
||||||
if self.packet_expected.header == data[: HEADER_SIZE - 4]:
|
if self.packet_expected.header == data[:HEADER_SIZE]:
|
||||||
self.logger.debug("valid packet received")
|
|
||||||
return VbanRtPacket(
|
return VbanRtPacket(
|
||||||
_kind=self._remote.kind,
|
_kind=self._remote.kind,
|
||||||
_voicemeeterType=data[28:29],
|
_voicemeeterType=data[28:29],
|
||||||
@@ -92,26 +113,17 @@ class Updater(threading.Thread):
|
|||||||
_stripLabelUTF8c60=data[452:932],
|
_stripLabelUTF8c60=data[452:932],
|
||||||
_busLabelUTF8c60=data[932:1412],
|
_busLabelUTF8c60=data[932:1412],
|
||||||
)
|
)
|
||||||
except TimeoutError:
|
except TimeoutError as e:
|
||||||
err_msg = f"Unable to establish connection with {self._remote.ip}"
|
self.logger.exception(f"{type(e).__name__}: {e}")
|
||||||
print(err_msg)
|
raise VBANCMDConnectionError(
|
||||||
raise VBANCMDError(err_msg)
|
f"timeout waiting for RtPacket from {self._remote.ip}"
|
||||||
|
) from e
|
||||||
|
|
||||||
def _get_rt(self) -> VbanRtPacket:
|
def stopped(self):
|
||||||
"""Attempt to fetch data packet until a valid one found"""
|
return self.stop_event.is_set()
|
||||||
|
|
||||||
def fget():
|
def run(self):
|
||||||
data = None
|
while not self.stopped():
|
||||||
while not data:
|
|
||||||
data = self._fetch_rt_packet()
|
|
||||||
time.sleep(self._remote.DELAY)
|
|
||||||
return data
|
|
||||||
|
|
||||||
return fget()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
while self._remote.running:
|
|
||||||
start = time.time()
|
|
||||||
_pp = self._get_rt()
|
_pp = self._get_rt()
|
||||||
pdirty = _pp.pdirty(self._remote.public_packet)
|
pdirty = _pp.pdirty(self._remote.public_packet)
|
||||||
ldirty = _pp.ldirty(
|
ldirty = _pp.ldirty(
|
||||||
@@ -119,24 +131,54 @@ class Updater(threading.Thread):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if pdirty or ldirty:
|
if pdirty or ldirty:
|
||||||
self.logger.debug("dirty state, updating public packet")
|
|
||||||
self._remote._public_packet = _pp
|
self._remote._public_packet = _pp
|
||||||
self._remote._pdirty = pdirty
|
self._remote._pdirty = pdirty
|
||||||
self._remote._ldirty = ldirty
|
self._remote._ldirty = ldirty
|
||||||
|
|
||||||
if self._remote.event.pdirty and self._remote.pdirty:
|
if self._remote.event.pdirty:
|
||||||
self._remote.subject.notify("pdirty")
|
self.queue.put("pdirty")
|
||||||
if self._remote.event.ldirty and self._remote.ldirty:
|
if self._remote.event.ldirty:
|
||||||
self._remote._strip_comp, self._remote._bus_comp = (
|
self.queue.put("ldirty")
|
||||||
_pp._strip_comp,
|
time.sleep(self._remote.ratelimit)
|
||||||
_pp._bus_comp,
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
)
|
self.queue.put(None)
|
||||||
self._remote.cache["strip_level"], self._remote.cache["bus_level"] = (
|
|
||||||
_pp.inputlevels,
|
|
||||||
_pp.outputlevels,
|
|
||||||
)
|
|
||||||
self._remote.subject.notify("ldirty")
|
|
||||||
|
|
||||||
elapsed = time.time() - start
|
|
||||||
if self._remote.ratelimit - elapsed > 0:
|
class Updater(threading.Thread):
|
||||||
time.sleep(self._remote.ratelimit - elapsed)
|
"""
|
||||||
|
continously updates the public packet
|
||||||
|
|
||||||
|
notifies observers of event updates
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, remote, queue):
|
||||||
|
super().__init__(name="updater", daemon=True)
|
||||||
|
self._remote = remote
|
||||||
|
self.queue = queue
|
||||||
|
self.logger = logger.getChild(self.__class__.__name__)
|
||||||
|
self._remote._strip_comp = [False] * (self._remote.kind.num_strip_levels)
|
||||||
|
self._remote._bus_comp = [False] * (self._remote.kind.num_bus_levels)
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
"""
|
||||||
|
Continously update observers of dirty states.
|
||||||
|
|
||||||
|
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||||
|
"""
|
||||||
|
while event := self.queue.get():
|
||||||
|
if event == "pdirty" and self._remote.pdirty:
|
||||||
|
self._remote.subject.notify(event)
|
||||||
|
elif event == "ldirty" and self._remote.ldirty:
|
||||||
|
self._remote._strip_comp, self._remote._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_packet.inputlevels,
|
||||||
|
self._remote._public_packet.outputlevels,
|
||||||
|
)
|
||||||
|
self._remote.subject.notify(event)
|
||||||
|
self.logger.debug(f"terminating {self.name} thread")
|
||||||
|
|||||||
Reference in New Issue
Block a user