mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-20 05:23:33 +00:00
Compare commits
14 Commits
da1d5132a8
...
add-to-bus
| Author | SHA1 | Date | |
|---|---|---|---|
| 714d2fc972 | |||
| c797912458 | |||
|
|
f702b4feb3 | ||
|
|
f8f10e358f | ||
| f7abc5248b | |||
| fec4315be2 | |||
| a3e3db3c37 | |||
| 3e201443e0 | |||
| 868017c79f | |||
| 795296d71e | |||
| e21a458c6f | |||
| b79d9494a2 | |||
| 328bea347c | |||
| 38bd284ba6 |
14
.gitignore
vendored
14
.gitignore
vendored
@@ -128,10 +128,12 @@ dmypy.json
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# test/config
|
||||
quick.py
|
||||
config.toml
|
||||
vm-api.log
|
||||
logging.json
|
||||
# test reports
|
||||
tests/reports/
|
||||
!tests/reports/badge-*.svg
|
||||
|
||||
.vscode/
|
||||
# test/config
|
||||
test-*.py
|
||||
config.toml
|
||||
|
||||
.vscode/
|
||||
|
||||
7
.pre-commit-config.yaml
Normal file
7
.pre-commit-config.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v2.3.0
|
||||
hooks:
|
||||
- id: check-yaml
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
16
README.md
16
README.md
@@ -2,9 +2,9 @@
|
||||
[](https://github.com/onyx-and-iris/voicemeeter-api-python/blob/dev/LICENSE)
|
||||
[](https://python-poetry.org/)
|
||||
[](https://github.com/astral-sh/ruff)
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
# Python Wrapper for Voicemeeter API
|
||||
|
||||
@@ -869,10 +869,12 @@ with voicemeeterlib.api('banana') as vm:
|
||||
|
||||
### Run tests
|
||||
|
||||
To run all tests:
|
||||
Install [poetry](https://python-poetry.org/docs/#installation) and then:
|
||||
|
||||
```
|
||||
pytest -v
|
||||
```powershell
|
||||
poetry poe test-basic
|
||||
poetry poe test-banana
|
||||
poetry poe test-potato
|
||||
```
|
||||
|
||||
### Official Documentation
|
||||
@@ -880,4 +882,4 @@ pytest -v
|
||||
- [Voicemeeter Remote C API](https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemoteAPI.pdf)
|
||||
|
||||
|
||||
[Voicemeeter Remote Header]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemote.h
|
||||
[Voicemeeter Remote Header]: https://github.com/onyx-and-iris/Voicemeeter-SDK/blob/main/VoicemeeterRemote.h
|
||||
|
||||
9
examples/eq_edit/README.md
Normal file
9
examples/eq_edit/README.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## About
|
||||
|
||||
The purpose of this script is to demonstratehow to utilize the channels and cells that are available as part of the EQ. It should take audio playing in the second virtual strip and then apply a LGF on the first physical at 500 Hz.
|
||||
|
||||
## Use
|
||||
|
||||
Configured for banana version.
|
||||
|
||||
Make sure you are playing audio into the second virtual strip and out of the first physical bus, both channels are unmuted and that you aren't monitoring another mixbus. Then run the script.
|
||||
21
examples/eq_edit/__main__.py
Normal file
21
examples/eq_edit/__main__.py
Normal file
@@ -0,0 +1,21 @@
|
||||
import time
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
def main():
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vm:
|
||||
vm.bus[0].eq.on = True
|
||||
vm.bus[0].eq.channel[0].cell[0].on = True
|
||||
vm.bus[0].eq.channel[0].cell[0].f = 500
|
||||
vm.bus[0].eq.channel[0].cell[0].type = 3 # Should correspond to LPF
|
||||
|
||||
time.sleep(3)
|
||||
vm.bus[0].eq.on = False
|
||||
vm.bus[0].eq.channel[0].cell[0].on = False
|
||||
vm.bus[0].eq.channel[0].cell[0].f = 50
|
||||
vm.bus[0].eq.channel[0].cell[0].type = 0
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,33 +1,8 @@
|
||||
## About
|
||||
# Events
|
||||
|
||||
This script demonstrates how to interact with the event thread/event object. It also demonstrates how to register event specific callbacks.
|
||||
If you want to receive updates on certain events there are two routes you can take:
|
||||
|
||||
By default the interface does not broadcast any events. So even though our callbacks are registered, and the event thread has been initiated, we won't receive updates.
|
||||
- Register a class that implements an `on_update(self, event) -> None` method on the `{Remote}.subject` class.
|
||||
- Register callback functions/methods on the `{Remote}.subject` class, one for each type of update.
|
||||
|
||||
After five seconds the event object is used to subscribe to all events for a total of thirty seconds.
|
||||
|
||||
Remember that events can also be unsubscribed to with `vm.event.remove()`. Callbacks can also be deregistered using vm.observer.remove().
|
||||
|
||||
The same can be done without a context manager:
|
||||
|
||||
```python
|
||||
vm = voicemeeterlib.api(KIND_ID)
|
||||
vm.login()
|
||||
vm.observer.add(on_midi) # register an `on_midi` callback function
|
||||
vm.init_thread()
|
||||
vm.event.add("midi") # in this case we only subscribe to midi events.
|
||||
...
|
||||
vm.end_thread()
|
||||
vm.logout()
|
||||
```
|
||||
|
||||
Once initialized, the event thread will continously run until end_thread() is called. Even if all events are unsubscribed to.
|
||||
|
||||
## Use
|
||||
|
||||
Simply run the script and trigger events and you should see the output after 5 seconds. To trigger events do the following:
|
||||
|
||||
- change GUI parameters to trigger pdirty
|
||||
- press any macrobutton to trigger mdirty
|
||||
- play audio through any bus to trigger ldirty
|
||||
- any midi input to trigger midi
|
||||
Included are examples of both approaches.
|
||||
|
||||
33
examples/events/callbacks/README.md
Normal file
33
examples/events/callbacks/README.md
Normal file
@@ -0,0 +1,33 @@
|
||||
## About
|
||||
|
||||
This script demonstrates how to interact with the event thread/event object. It also demonstrates how to register event specific callbacks.
|
||||
|
||||
By default the interface does not broadcast any events. So even though our callbacks are registered, and the event thread has been initiated, we won't receive updates.
|
||||
|
||||
After five seconds the event object is used to subscribe to all events for a total of thirty seconds.
|
||||
|
||||
Remember that events can also be unsubscribed to with `vm.event.remove()`. Callbacks can also be deregistered using vm.observer.remove().
|
||||
|
||||
The same can be done without a context manager:
|
||||
|
||||
```python
|
||||
vm = voicemeeterlib.api(KIND_ID)
|
||||
vm.login()
|
||||
vm.observer.add(on_midi) # register an `on_midi` callback function
|
||||
vm.init_thread()
|
||||
vm.event.add("midi") # in this case we only subscribe to midi events.
|
||||
...
|
||||
vm.end_thread()
|
||||
vm.logout()
|
||||
```
|
||||
|
||||
Once initialized, the event thread will continously run until end_thread() is called. Even if all events are unsubscribed to.
|
||||
|
||||
## Use
|
||||
|
||||
Simply run the script and trigger events and you should see the output after 5 seconds. To trigger events do the following:
|
||||
|
||||
- change GUI parameters to trigger pdirty
|
||||
- press any macrobutton to trigger mdirty
|
||||
- play audio through any bus to trigger ldirty
|
||||
- any midi input to trigger midi
|
||||
@@ -8,18 +8,18 @@ logging.basicConfig(level=logging.INFO)
|
||||
|
||||
class App:
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
self._vm = vm
|
||||
# register the callbacks for each event
|
||||
self.vm.observer.add(
|
||||
self._vm.observer.add(
|
||||
[self.on_pdirty, self.on_mdirty, self.on_ldirty, self.on_midi]
|
||||
)
|
||||
|
||||
def __enter__(self):
|
||||
self.vm.init_thread()
|
||||
self._vm.init_thread()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.vm.end_thread()
|
||||
self._vm.end_thread()
|
||||
|
||||
def on_pdirty(self):
|
||||
print('pdirty!')
|
||||
@@ -28,13 +28,13 @@ class App:
|
||||
print('mdirty!')
|
||||
|
||||
def on_ldirty(self):
|
||||
for bus in self.vm.bus:
|
||||
for bus in self._vm.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
|
||||
def on_midi(self):
|
||||
current = self.vm.midi.current
|
||||
print(f'Value of midi button {current} is {self.vm.midi.get(current)}')
|
||||
current = self._vm.midi.current
|
||||
print(f'Value of midi button {current} is {self._vm.midi.get(current)}')
|
||||
|
||||
|
||||
def main():
|
||||
45
examples/events/observer/__main__.py
Normal file
45
examples/events/observer/__main__.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import logging
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, vm):
|
||||
self._vm = vm
|
||||
# register your app as event observer
|
||||
self._vm.observer.add(self)
|
||||
|
||||
def __str__(self):
|
||||
return type(self).__name__
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, event):
|
||||
if event == 'pdirty':
|
||||
print('pdirty!')
|
||||
elif event == 'mdirty':
|
||||
print('mdirty!')
|
||||
elif event == 'ldirty':
|
||||
for bus in self._vm.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
elif event == 'midi':
|
||||
current = self._vm.midi.current
|
||||
print(f'Value of midi button {current} is {self._vm.midi.get(current)}')
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = 'banana'
|
||||
|
||||
with voicemeeterlib.api(
|
||||
KIND_ID, **{k: True for k in ('pdirty', 'mdirty', 'ldirty', 'midi')}
|
||||
) as vm:
|
||||
App(vm)
|
||||
|
||||
while _ := input('Press <Enter> to exit\n'):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -12,9 +12,9 @@ class App(tk.Tk):
|
||||
|
||||
def __init__(self, vm):
|
||||
super().__init__()
|
||||
self.vm = vm
|
||||
self._vm = vm
|
||||
self.title(f'{vm} - version {vm.version}')
|
||||
self.vm.observer.add(self.on_ldirty)
|
||||
self._vm.observer.add(self.on_ldirty)
|
||||
|
||||
# create widget variables
|
||||
self.button_var = tk.BooleanVar(value=vm.strip[self.INDEX].mute)
|
||||
@@ -31,7 +31,7 @@ class App(tk.Tk):
|
||||
)
|
||||
|
||||
# create labelframe and grid it onto the mainframe
|
||||
self.labelframe = tk.LabelFrame(self, text=self.vm.strip[self.INDEX].label)
|
||||
self.labelframe = tk.LabelFrame(self, text=self._vm.strip[self.INDEX].label)
|
||||
self.labelframe.grid(padx=1)
|
||||
|
||||
# create slider and grid it onto the labelframe
|
||||
@@ -76,12 +76,12 @@ class App(tk.Tk):
|
||||
|
||||
def on_slider_move(self, *args):
|
||||
val = round(self.slider_var.get(), 1)
|
||||
self.vm.strip[self.INDEX].gain = val
|
||||
self._vm.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.vm.strip[self.INDEX].mute = self.button_var.get()
|
||||
self._vm.strip[self.INDEX].mute = self.button_var.get()
|
||||
self.style.configure(
|
||||
'Mute.TButton', foreground='#cd5c5c' if self.button_var.get() else '#5a5a5a'
|
||||
)
|
||||
@@ -89,10 +89,10 @@ class App(tk.Tk):
|
||||
def on_button_double_click(self, e):
|
||||
self.slider_var.set(0)
|
||||
self.gainlabel_var.set(0)
|
||||
self.vm.strip[self.INDEX].gain = 0
|
||||
self._vm.strip[self.INDEX].gain = 0
|
||||
|
||||
def _get_level(self):
|
||||
val = max(self.vm.strip[self.INDEX].levels.postfader)
|
||||
val = max(self._vm.strip[self.INDEX].levels.postfader)
|
||||
return 0 if self.button_var.get() else 72 + val - 12
|
||||
|
||||
def on_ldirty(self):
|
||||
|
||||
@@ -10,31 +10,31 @@ class App:
|
||||
MACROBUTTON = 0
|
||||
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
self.vm.observer.add(self.on_midi)
|
||||
self._vm = vm
|
||||
self._vm.observer.add(self.on_midi)
|
||||
|
||||
def on_midi(self):
|
||||
if self.get_info() == self.MIDI_BUTTON:
|
||||
self.on_midi_press()
|
||||
|
||||
def get_info(self):
|
||||
current = self.vm.midi.current
|
||||
print(f'Value of midi button {current} is {self.vm.midi.get(current)}')
|
||||
current = self._vm.midi.current
|
||||
print(f'Value of midi button {current} is {self._vm.midi.get(current)}')
|
||||
return current
|
||||
|
||||
def on_midi_press(self):
|
||||
"""if midi button 48 is pressed and strip 3 level max > -40, then set trigger for macrobutton 0"""
|
||||
|
||||
if (
|
||||
self.vm.midi.get(self.MIDI_BUTTON) == 127
|
||||
and max(self.vm.strip[3].levels.postfader) > -40
|
||||
self._vm.midi.get(self.MIDI_BUTTON) == 127
|
||||
and max(self._vm.strip[3].levels.postfader) > -40
|
||||
):
|
||||
print(
|
||||
f'Strip 3 level max is greater than -40 and midi button {self.MIDI_BUTTON} is pressed'
|
||||
)
|
||||
self.vm.button[self.MACROBUTTON].trigger = True
|
||||
self._vm.button[self.MACROBUTTON].trigger = True
|
||||
else:
|
||||
self.vm.button[self.MACROBUTTON].trigger = False
|
||||
self._vm.button[self.MACROBUTTON].trigger = False
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
@@ -21,15 +21,20 @@ config.dictConfig(
|
||||
}
|
||||
},
|
||||
'loggers': {
|
||||
'voicemeeterlib.iremote': {'handlers': ['stream'], 'level': 'DEBUG'}
|
||||
'voicemeeterlib.iremote': {
|
||||
'handlers': ['stream'],
|
||||
'level': 'DEBUG',
|
||||
'propagate': False,
|
||||
}
|
||||
},
|
||||
'root': {'handlers': ['stream'], 'level': 'WARNING'},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class MyClient:
|
||||
def __init__(self, vm, stop_event):
|
||||
self.vm = vm
|
||||
self._vm = vm
|
||||
self._stop_event = stop_event
|
||||
self._client = obsws.EventClient()
|
||||
self._client.callback.register(
|
||||
@@ -46,16 +51,16 @@ class MyClient:
|
||||
self._client.disconnect()
|
||||
|
||||
def on_start(self):
|
||||
self.vm.strip[0].mute = True
|
||||
self.vm.strip[1].B1 = True
|
||||
self.vm.strip[2].B2 = True
|
||||
self._vm.strip[0].mute = True
|
||||
self._vm.strip[1].B1 = True
|
||||
self._vm.strip[2].B2 = True
|
||||
|
||||
def on_brb(self):
|
||||
self.vm.strip[7].fadeto(0, 500)
|
||||
self.vm.bus[0].mute = True
|
||||
self._vm.strip[7].fadeto(0, 500)
|
||||
self._vm.bus[0].mute = True
|
||||
|
||||
def on_end(self):
|
||||
self.vm.apply(
|
||||
self._vm.apply(
|
||||
{
|
||||
'strip-0': {'mute': True, 'comp': {'ratio': 4.3}},
|
||||
'strip-1': {'mute': True, 'B1': False, 'gate': {'attack': 2.3}},
|
||||
@@ -65,10 +70,10 @@ class MyClient:
|
||||
)
|
||||
|
||||
def on_live(self):
|
||||
self.vm.strip[0].mute = False
|
||||
self.vm.strip[7].fadeto(-6, 500)
|
||||
self.vm.strip[7].A3 = True
|
||||
self.vm.vban.instream[0].on = True
|
||||
self._vm.strip[0].mute = False
|
||||
self._vm.strip[7].fadeto(-6, 500)
|
||||
self._vm.strip[7].A3 = True
|
||||
self._vm.vban.instream[0].on = True
|
||||
|
||||
def on_current_program_scene_changed(self, data):
|
||||
scene = data.scene_name
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import logging
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class App:
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
# register your app as event observer
|
||||
self.vm.observer.add(self)
|
||||
|
||||
def __str__(self):
|
||||
return type(self).__name__
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, event):
|
||||
if event == "pdirty":
|
||||
print("pdirty!")
|
||||
elif event == "mdirty":
|
||||
print("mdirty!")
|
||||
elif event == "ldirty":
|
||||
for bus in self.vm.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
elif event == "midi":
|
||||
current = self.vm.midi.current
|
||||
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
|
||||
|
||||
|
||||
def main():
|
||||
KIND_ID = "banana"
|
||||
|
||||
with voicemeeterlib.api(
|
||||
KIND_ID, **{k: True for k in ("pdirty", "mdirty", "ldirty", "midi")}
|
||||
) as vm:
|
||||
App(vm)
|
||||
|
||||
while _ := input("Press <Enter> to exit\n"):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
18
poetry.lock
generated
18
poetry.lock
generated
@@ -172,14 +172,14 @@ testing = ["covdefaults (>=2.3)", "pytest (>=8.3.3)", "pytest-cov (>=5)", "pytes
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.4.4"
|
||||
version = "8.3.4"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"},
|
||||
{file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"},
|
||||
{file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"},
|
||||
{file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -187,11 +187,11 @@ colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
|
||||
iniconfig = "*"
|
||||
packaging = "*"
|
||||
pluggy = ">=0.12,<2.0"
|
||||
tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
|
||||
pluggy = ">=1.5,<2"
|
||||
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
||||
|
||||
[package.extras]
|
||||
testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-randomly"
|
||||
@@ -359,5 +359,5 @@ virtualenv = "*"
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "<4.0,>=3.10"
|
||||
content-hash = "fd3332b6e69588ff2902930c08a7882610bb8b18430f8b41edb11420ad2b597d"
|
||||
python-versions = ">=3.10"
|
||||
content-hash = "6339967c3f6cad8e4db7047ef3d12a5b059a279d0f7c98515c961477680bab8f"
|
||||
|
||||
@@ -7,7 +7,7 @@ authors = [
|
||||
]
|
||||
license = {text = "MIT"}
|
||||
readme = "README.md"
|
||||
requires-python = "<4.0,>=3.10"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"tomli (>=2.0.1,<3.0) ; python_version < '3.11'",
|
||||
]
|
||||
@@ -19,8 +19,8 @@ packages = [{ include = "voicemeeterlib" }]
|
||||
poethepoet = "^0.32.1"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
pytest = "^7.4.4"
|
||||
pytest-randomly = "^3.12.0"
|
||||
pytest = "^8.3.4"
|
||||
pytest-randomly = "^3.16.0"
|
||||
ruff = "^0.8.6"
|
||||
tox = "^4.23.2"
|
||||
virtualenv-pyenv = "^0.5.0"
|
||||
@@ -31,45 +31,18 @@ build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poe.tasks]
|
||||
dsl.script = "scripts:ex_dsl"
|
||||
events.script = "scripts:ex_events"
|
||||
callbacks.script = "scripts:ex_callbacks"
|
||||
gui.script = "scripts:ex_gui"
|
||||
levels.script = "scripts:ex_levels"
|
||||
midi.script = "scripts:ex_midi"
|
||||
obs.script = "scripts:ex_obs"
|
||||
observer.script = "scripts:ex_observer"
|
||||
basic.script = "scripts:test_basic"
|
||||
banana.script = "scripts:test_banana"
|
||||
potato.script = "scripts:test_potato"
|
||||
all.script = "scripts:test_all"
|
||||
test-basic.script = "scripts:test_basic"
|
||||
test-banana.script = "scripts:test_banana"
|
||||
test-potato.script = "scripts:test_potato"
|
||||
test-all.script = "scripts:test_all"
|
||||
generate-badges.script = "scripts:generate_badges"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox]
|
||||
envlist = py310,py311,py312
|
||||
|
||||
[testenv]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
commands =
|
||||
poetry install -v
|
||||
poetry run pytest tests/
|
||||
|
||||
[testenv:dsl]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps = pyparsing
|
||||
commands =
|
||||
poetry install -v --without dev
|
||||
poetry run python examples/dsl/
|
||||
|
||||
[testenv:obs]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps = obsws-python
|
||||
commands =
|
||||
poetry install -v --without dev
|
||||
poetry run python examples/obs/
|
||||
"""
|
||||
|
||||
[tool.ruff]
|
||||
exclude = [
|
||||
@@ -105,14 +78,16 @@ target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default.
|
||||
# Enable flake8-errmsg (EM) warnings.
|
||||
# Enable flake8-bugbear (B) warnings.
|
||||
# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or
|
||||
# McCabe complexity (`C901`) by default.
|
||||
select = ["E4", "E7", "E9", "F"]
|
||||
select = ["E4", "E7", "E9", "EM", "F", "B"]
|
||||
ignore = []
|
||||
|
||||
# Allow fix for all enabled rules (when `--fix`) is provided.
|
||||
fixable = ["ALL"]
|
||||
unfixable = []
|
||||
unfixable = ["B"]
|
||||
|
||||
# Allow unused variables when underscore-prefixed.
|
||||
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
|
||||
|
||||
35
scripts.py
35
scripts.py
@@ -5,53 +5,58 @@ from pathlib import Path
|
||||
|
||||
|
||||
def ex_dsl():
|
||||
subprocess.run(["tox", "r", "-e", "dsl"])
|
||||
subprocess.run(['tox', 'r', '-e', 'dsl'])
|
||||
|
||||
|
||||
def ex_events():
|
||||
scriptpath = Path.cwd() / "examples" / "events" / "."
|
||||
def ex_callbacks():
|
||||
scriptpath = Path.cwd() / 'examples' / 'events' / 'callbacks' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_gui():
|
||||
scriptpath = Path.cwd() / "examples" / "gui" / "."
|
||||
scriptpath = Path.cwd() / 'examples' / 'gui' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_levels():
|
||||
scriptpath = Path.cwd() / "examples" / "levels" / "."
|
||||
scriptpath = Path.cwd() / 'examples' / 'levels' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_midi():
|
||||
scriptpath = Path.cwd() / "examples" / "midi" / "."
|
||||
scriptpath = Path.cwd() / 'examples' / 'midi' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
subprocess.run(["tox", "r", "-e", "obs"])
|
||||
subprocess.run(['tox', 'r', '-e', 'obs'])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
scriptpath = Path.cwd() / "examples" / "observer" / "."
|
||||
scriptpath = Path.cwd() / 'examples' / 'events' / 'observer' / '.'
|
||||
subprocess.run([sys.executable, str(scriptpath)])
|
||||
|
||||
|
||||
def test_basic():
|
||||
os.environ["KIND"] = "basic"
|
||||
subprocess.run(["tox"])
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'basic'})
|
||||
|
||||
|
||||
def test_banana():
|
||||
os.environ["KIND"] = "banana"
|
||||
subprocess.run(["tox"])
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'banana'})
|
||||
|
||||
|
||||
def test_potato():
|
||||
os.environ["KIND"] = "potato"
|
||||
subprocess.run(["tox"])
|
||||
subprocess.run(['tox'], env=os.environ.copy() | {'KIND': 'potato'})
|
||||
|
||||
|
||||
def test_all():
|
||||
steps = [test_basic, test_banana, test_potato]
|
||||
[step() for step in steps]
|
||||
for step in steps:
|
||||
step()
|
||||
|
||||
|
||||
def generate_badges():
|
||||
for kind in ['basic', 'banana', 'potato']:
|
||||
subprocess.run(
|
||||
['tox', 'r', '-e', 'genbadge'], env=os.environ.copy() | {'KIND': kind}
|
||||
)
|
||||
|
||||
@@ -31,10 +31,8 @@ class Data:
|
||||
return (2 * self.phys_in) + (8 * self.virt_in)
|
||||
|
||||
|
||||
# get KIND_ID from env var, otherwise set to random
|
||||
KIND_ID = os.environ.get(
|
||||
"KIND", random.choice(tuple(kind_id.name.lower() for kind_id in KindId))
|
||||
)
|
||||
# get KIND from environment, if not set default to potato
|
||||
KIND_ID = os.environ.get('KIND', 'potato')
|
||||
vm = voicemeeterlib.api(KIND_ID)
|
||||
kind = kindmap(KIND_ID)
|
||||
|
||||
@@ -56,7 +54,7 @@ data = Data(
|
||||
|
||||
|
||||
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)
|
||||
vm.login()
|
||||
vm.command.reset()
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
Function RunTests {
|
||||
$coverage = "./tests/pytest_coverage.log"
|
||||
$run_tests = "pytest --run-slow -v --capture=tee-sys --junitxml=./tests/.coverage.xml"
|
||||
$match_pattern = "^=|^\s*$|^Running|^Using|^plugins|^collecting|^tests"
|
||||
|
||||
if ( Test-Path $coverage ) { Clear-Content $coverage }
|
||||
|
||||
ForEach ($line in $(Invoke-Expression $run_tests)) {
|
||||
If ( $line -Match $match_pattern ) {
|
||||
if ( $line -Match "^Running tests for kind \[(\w+)\]" ) { $kind = $Matches[1] }
|
||||
$line | Tee-Object -FilePath $coverage -Append
|
||||
}
|
||||
}
|
||||
Write-Output "$(Get-TimeStamp)" | Out-File $coverage -Append
|
||||
|
||||
Invoke-Expression "genbadge tests -t 90 -i ./tests/.coverage.xml -o ./tests/$kind.svg"
|
||||
}
|
||||
|
||||
Function Get-TimeStamp {
|
||||
|
||||
return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
|
||||
|
||||
}
|
||||
|
||||
if ($MyInvocation.InvocationName -ne ".") {
|
||||
Invoke-Expression ".\.venv\Scripts\Activate.ps1"
|
||||
|
||||
@("potato") | ForEach-Object {
|
||||
$env:KIND = $_
|
||||
RunTests
|
||||
}
|
||||
|
||||
|
||||
Invoke-Expression "deactivate"
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 184"><title>tests: 184</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">184</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">184</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 158"><title>tests: 158</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">158</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">158</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 159"><title>tests: 159</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">159</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">159</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 115"><title>tests: 115</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">115</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">115</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 116"><title>tests: 116</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">116</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">116</text></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="68" height="20" role="img" aria-label="tests: 183"><title>tests: 183</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="68" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="37" height="20" fill="#555"/><rect x="37" width="31" height="20" fill="#4c1"/><rect width="68" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="195" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="270">tests</text><text x="195" y="140" transform="scale(.1)" fill="#fff" textLength="270">tests</text><text aria-hidden="true" x="515" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">183</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">183</text></g></svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
42
tox.ini
Normal file
42
tox.ini
Normal file
@@ -0,0 +1,42 @@
|
||||
[tox]
|
||||
envlist = py310,py311,py312,py313
|
||||
|
||||
[testenv]
|
||||
passenv = *
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
commands_pre =
|
||||
poetry install --no-interaction --no-root
|
||||
commands =
|
||||
poetry run pytest tests
|
||||
|
||||
[testenv:genbadge]
|
||||
passenv = *
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps =
|
||||
genbadge[all]
|
||||
pytest-html
|
||||
commands_pre =
|
||||
poetry install --no-interaction --no-root
|
||||
commands =
|
||||
poetry run pytest --capture=tee-sys --junitxml=./tests/reports/junit-${KIND}.xml --html=./tests/reports/${KIND}.html tests
|
||||
poetry run genbadge tests -t 90 -i ./tests/reports/junit-${KIND}.xml -o ./tests/reports/badge-${KIND}.svg
|
||||
|
||||
[testenv:dsl]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps = pyparsing
|
||||
commands_pre =
|
||||
poetry install --no-interaction --no-root --without dev
|
||||
commands =
|
||||
poetry run python examples/dsl
|
||||
|
||||
[testenv:obs]
|
||||
setenv = VIRTUALENV_DISCOVERY=pyenv
|
||||
allowlist_externals = poetry
|
||||
deps = obsws-python
|
||||
commands_pre =
|
||||
poetry install --no-interaction --no-root --without dev
|
||||
commands =
|
||||
poetry run python examples/obs
|
||||
@@ -88,6 +88,24 @@ class Bus(IRemote):
|
||||
|
||||
|
||||
class BusEQ(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i):
|
||||
"""
|
||||
Factory method for BusEQ.
|
||||
|
||||
Returns a BusEQ class.
|
||||
"""
|
||||
kls = (cls,)
|
||||
return type(
|
||||
'BusEQ',
|
||||
kls,
|
||||
{
|
||||
'channel': tuple(
|
||||
BusEQCh.make(remote, i, j) for j in range(remote.kind.channels)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Bus[{self.index}].eq'
|
||||
@@ -109,6 +127,85 @@ class BusEQ(IRemote):
|
||||
self.setter('ab', 1 if val else 0)
|
||||
|
||||
|
||||
class BusEQCh(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i, j):
|
||||
"""
|
||||
Factory method for Bus EQ channel.
|
||||
|
||||
Returns a BusEQCh class.
|
||||
"""
|
||||
kls = (cls,)
|
||||
return type(
|
||||
'BusEQCh',
|
||||
kls,
|
||||
{
|
||||
'cell': tuple(
|
||||
BusEQChCell(remote, i, j, k) for k in range(remote.kind.cells)
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
def __init__(self, remote, i, j):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Bus[{self.index}].eq.channel[{self.channel_index}]'
|
||||
|
||||
|
||||
class BusEQChCell(IRemote):
|
||||
def __init__(self, remote, i, j, k):
|
||||
super().__init__(remote, i)
|
||||
self.channel_index = j
|
||||
self.cell_index = k
|
||||
|
||||
@property
|
||||
def identifier(self) -> str:
|
||||
return f'Bus[{self.index}].eq.channel[{self.channel_index}].cell[{self.cell_index}]'
|
||||
|
||||
@property
|
||||
def on(self) -> bool:
|
||||
return self.getter('on') == 1
|
||||
|
||||
@on.setter
|
||||
def on(self, val: bool):
|
||||
self.setter('on', 1 if val else 0)
|
||||
|
||||
@property
|
||||
def type(self) -> int:
|
||||
return int(self.getter('type'))
|
||||
|
||||
@type.setter
|
||||
def type(self, val: int):
|
||||
self.setter('type', val)
|
||||
|
||||
@property
|
||||
def f(self) -> float:
|
||||
return round(self.getter('f'), 1)
|
||||
|
||||
@f.setter
|
||||
def f(self, val: float):
|
||||
self.setter('f', val)
|
||||
|
||||
@property
|
||||
def gain(self) -> float:
|
||||
return round(self.getter('gain'), 1)
|
||||
|
||||
@gain.setter
|
||||
def gain(self, val: float):
|
||||
self.setter('gain', val)
|
||||
|
||||
@property
|
||||
def q(self) -> float:
|
||||
return round(self.getter('q'), 1)
|
||||
|
||||
@q.setter
|
||||
def q(self, val: float):
|
||||
self.setter('q', val)
|
||||
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
@classmethod
|
||||
def make(cls, remote, i, kind):
|
||||
@@ -321,7 +418,7 @@ def bus_factory(is_phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
{
|
||||
'levels': BusLevel(remote, i),
|
||||
'mode': BUSMODEMIXIN_cls(remote, i),
|
||||
'eq': BusEQ(remote, i),
|
||||
'eq': BusEQ.make(remote, i),
|
||||
},
|
||||
)(remote, i)
|
||||
|
||||
|
||||
@@ -31,6 +31,8 @@ class KindMapClass(metaclass=SingletonType):
|
||||
asio: tuple
|
||||
insert: int
|
||||
composite: int
|
||||
channels: int
|
||||
cells: int
|
||||
|
||||
@property
|
||||
def phys_in(self) -> int:
|
||||
@@ -76,6 +78,8 @@ class BasicMap(KindMapClass):
|
||||
asio: tuple = (0, 0)
|
||||
insert: int = 0
|
||||
composite: int = 0
|
||||
channels: int = 0
|
||||
cells: int = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -86,6 +90,8 @@ class BananaMap(KindMapClass):
|
||||
asio: tuple = (6, 8)
|
||||
insert: int = 22
|
||||
composite: int = 8
|
||||
channels: int = 9
|
||||
cells: int = 6
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -96,6 +102,8 @@ class PotatoMap(KindMapClass):
|
||||
asio: tuple = (10, 8)
|
||||
insert: int = 34
|
||||
composite: int = 8
|
||||
channels: int = 9
|
||||
cells: int = 6
|
||||
|
||||
|
||||
def kind_factory(kind_id):
|
||||
|
||||
Reference in New Issue
Block a user