mirror of
https://github.com/onyx-and-iris/voicemeeter-api-python.git
synced 2026-04-06 23:43:30 +00:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e6ea1e5f4f | |||
|
|
a460c6aeb0 | ||
|
|
bc508f8982 | ||
|
|
a4cc7058b6 | ||
|
|
6fa6d70f9b | ||
|
|
a73ebf364b | ||
|
|
caf05aa789 | ||
|
|
405fa8d5cb | ||
|
|
5ad5622612 | ||
|
|
108c327c52 | ||
|
|
7f1a51f86d | ||
|
|
94bace4f4d | ||
|
|
4e8532e805 | ||
|
|
907df78b37 | ||
|
|
f4fc58cea0 | ||
|
|
816fd76213 | ||
|
|
ad69d2cf14 | ||
|
|
86612a65cb | ||
|
|
08fdad135d | ||
|
|
30370f70ee | ||
|
|
f62a22f563 | ||
|
|
c513e4db19 | ||
|
|
9c8fe0b626 | ||
|
|
af0d51eeb1 | ||
|
|
bd686ef67d | ||
|
|
aefde48c98 | ||
|
|
4c6fc2d396 | ||
|
|
eddccb66c5 | ||
|
|
81a74d136c | ||
|
|
6b7a79173c | ||
|
|
ef0c94a6f1 | ||
|
|
a54a232a82 | ||
|
|
b2156ffade | ||
|
|
496f9d37fa | ||
|
|
3f9c486fa0 | ||
|
|
48b2857c58 | ||
|
|
af0740ddec | ||
|
|
f3eec58c25 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -131,3 +131,5 @@ dmypy.json
|
||||
# test/config
|
||||
quick.py
|
||||
config.toml
|
||||
|
||||
.vscode/
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
@@ -11,6 +11,61 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
|
||||
|
||||
- [x]
|
||||
|
||||
## [1.0.0] - 2023-06-19
|
||||
|
||||
No changes to the codebase but it has been stable for several months and should already have been bumped to major version 1.0
|
||||
|
||||
I will move this commit to a separate branch in preparation for version 2.0.
|
||||
|
||||
## [0.9.0] - 2022-10-11
|
||||
|
||||
### Added
|
||||
|
||||
- StripDevice and BusDevice mixins.
|
||||
- README updated to reflect changes.
|
||||
- Minor version bump
|
||||
|
||||
### Removed
|
||||
|
||||
- device, sr properties for physical strip, bus moved into mixin classes
|
||||
|
||||
### Changed
|
||||
|
||||
- Event class property setters added.
|
||||
- Event add/remove methods now accept multiple events.
|
||||
- bus levels now printed in observer example.
|
||||
|
||||
### Fixed
|
||||
|
||||
- initialize channel comps in updater thread. Fixes bug when switching to a kind before any level updates have occurred.
|
||||
|
||||
## [0.8.0] - 2022-09-29
|
||||
|
||||
### Added
|
||||
|
||||
- Logging level INFO set on all examples.
|
||||
- Minor version bump
|
||||
- vm.subject subsection added to README
|
||||
|
||||
### Changed
|
||||
|
||||
- Logging module used in place of print statements across the interface.
|
||||
- time.time() now used to steady rate of updates in updater thread.
|
||||
|
||||
### Fixed
|
||||
|
||||
- call to cache bug in updater thread
|
||||
|
||||
## [0.7.0] - 2022-09-03
|
||||
|
||||
### Added
|
||||
|
||||
- tomli/tomllib compatibility layer to support python 3.10
|
||||
|
||||
### Removed
|
||||
|
||||
- 3.10 branch
|
||||
|
||||
## [0.6.0] - 2022-08-02
|
||||
|
||||
### Added
|
||||
|
||||
155
README.md
155
README.md
@@ -14,21 +14,17 @@ For an outline of past/future changes refer to: [CHANGELOG](CHANGELOG.md)
|
||||
|
||||
## Tested against
|
||||
|
||||
- Basic 1.0.8.2
|
||||
- Banana 2.0.6.2
|
||||
- Potato 3.0.2.2
|
||||
- Basic 1.0.8.4
|
||||
- Banana 2.0.6.4
|
||||
- Potato 3.0.2.4
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Voicemeeter](https://voicemeeter.com/)
|
||||
- Python 3.11 or greater
|
||||
- Python 3.10 or greater
|
||||
|
||||
## Installation
|
||||
|
||||
### `Pip`
|
||||
|
||||
Install voicemeeter-api package from your console
|
||||
|
||||
`pip install voicemeeter-api`
|
||||
|
||||
## `Use`
|
||||
@@ -55,12 +51,12 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vm.bus[3].gain = -6.3
|
||||
self.vm.bus[4].eq = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vm.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vm.bus[4].eq}",
|
||||
)
|
||||
self.vm.bus[3].gain = -6.3
|
||||
self.vm.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
@@ -114,8 +110,6 @@ The following properties are available.
|
||||
- `limit`: int, from -40 to 12
|
||||
- `A1 - A5`, `B1 - B3`: boolean
|
||||
- `label`: string
|
||||
- `device`: string
|
||||
- `sr`: int
|
||||
- `mc`: boolean
|
||||
- `k`: int, from 0 to 4
|
||||
- `bass`: float, from -12.0 to 12.0
|
||||
@@ -143,7 +137,7 @@ vm.strip[3].gain = 3.7
|
||||
print(vm.strip[0].label)
|
||||
```
|
||||
|
||||
The following methods are Available.
|
||||
The following methods are available.
|
||||
|
||||
- `appgain(name, value)`: string, float, from 0.0 to 1.0
|
||||
|
||||
@@ -199,8 +193,6 @@ The following properties are available.
|
||||
- `sel`: boolean
|
||||
- `gain`: float, from -60.0 to 12.0
|
||||
- `label`: string
|
||||
- `device`: string
|
||||
- `sr`: int
|
||||
- `returnreverb`: float, from 0.0 to 10.0
|
||||
- `returndelay`: float, from 0.0 to 10.0
|
||||
- `returnfx1`: float, from 0.0 to 10.0
|
||||
@@ -274,6 +266,28 @@ vm.strip[0].fadeto(-10.3, 1000)
|
||||
vm.bus[3].fadeby(-5.6, 500)
|
||||
```
|
||||
|
||||
#### Strip.Device | Bus.Device
|
||||
|
||||
The following properties are available
|
||||
|
||||
- `name`: str
|
||||
- `sr`: int
|
||||
- `wdm`: str
|
||||
- `ks`: str
|
||||
- `mme`: str
|
||||
- `asio`: str
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
print(vm.strip[0].device.name)
|
||||
vm.bus[0].device.asio = "Audient USB Audio ASIO Driver"
|
||||
```
|
||||
|
||||
strip|bus device parameters are defined for physical channels only.
|
||||
|
||||
name, sr are read only. wdm, ks, mme, asio are write only.
|
||||
|
||||
### Macrobuttons
|
||||
|
||||
The following properties are available.
|
||||
@@ -579,9 +593,76 @@ with voicemeeterlib.api('banana') as vm:
|
||||
|
||||
will load a user config file at configs/banana/example.toml for Voicemeeter Banana.
|
||||
|
||||
## `Base Module`
|
||||
## Events
|
||||
|
||||
### Remote class
|
||||
Level updates are considered high volume, by default they are NOT listened for. Use subs keyword arg to initialize event updates.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
# Set updates to occur every 50ms
|
||||
# Listen for level updates but disable midi updates
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, subs={"ldirty": True, "midi": False}) as vm:
|
||||
...
|
||||
```
|
||||
|
||||
#### `vm.subject`
|
||||
|
||||
Use the Subject class to register an app as event observer.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add`: registers an app as an event observer
|
||||
- `remove`: deregisters an app as an event observer
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
# register an app to receive updates
|
||||
class App():
|
||||
def __init__(self, vm):
|
||||
vm.subject.add(self)
|
||||
...
|
||||
```
|
||||
|
||||
#### `vm.event`
|
||||
|
||||
Use the event class to toggle updates as necessary.
|
||||
|
||||
The following properties are available:
|
||||
|
||||
- `pdirty`: boolean
|
||||
- `mdirty`: boolean
|
||||
- `midi`: boolean
|
||||
- `ldirty`: boolean
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vm.event.ldirty = True
|
||||
|
||||
vm.event.pdirty = False
|
||||
```
|
||||
|
||||
Or add, remove a list of events.
|
||||
|
||||
The following methods are available:
|
||||
|
||||
- `add()`
|
||||
- `remove()`
|
||||
- `get()`
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vm.event.remove(["pdirty", "mdirty", "midi"])
|
||||
|
||||
# get a list of currently subscribed
|
||||
print(vm.event.get())
|
||||
```
|
||||
|
||||
## Remote class
|
||||
|
||||
`voicemeeterlib.api(kind_id: str)`
|
||||
|
||||
@@ -595,46 +676,6 @@ You may pass the following optional keyword arguments:
|
||||
- `midi`: midi updates
|
||||
- `ldirty`: level updates
|
||||
|
||||
#### Event updates
|
||||
|
||||
To receive event updates you should do the following:
|
||||
|
||||
- register your app to receive updates using the `vm.subject.add(observer)` method, where observer is your app.
|
||||
- define an `on_update(subject)` callback function in your app. The value of subject may be checked for the type of event.
|
||||
|
||||
See `examples/observer` for a demonstration.
|
||||
|
||||
Level updates are considered high volume, by default they are NOT listened for. However, polling them with strip.levels and bus.levels methods will still work.
|
||||
|
||||
So if you don't wish to receive level updates, or you prefer to handle them yourself simply leave ldirty as default (False).
|
||||
|
||||
Each of the update types may be enabled/disabled separately. Don't use a midi controller? You have the option to disable midi updates.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
import voicemeeterlib
|
||||
# Set updates to occur every 50ms
|
||||
# Listen for level updates but disable midi updates
|
||||
with voicemeeterlib.api('banana', ratelimit=0.05, subs={"ldirty": True, "midi": False}) as vm:
|
||||
...
|
||||
```
|
||||
|
||||
#### `vm.event`
|
||||
|
||||
You may also add/remove event subscriptions as necessary with the Event class.
|
||||
|
||||
example:
|
||||
|
||||
```python
|
||||
vm.event.add("ldirty")
|
||||
|
||||
vm.event.remove("pdirty")
|
||||
|
||||
# get a list of currently subscribed
|
||||
print(vm.event.get())
|
||||
```
|
||||
|
||||
Access to lower level Getters and Setters are provided with these functions:
|
||||
|
||||
- `vm.get(param, is_string=False)`: For getting the value of any parameter. Set string to True if getting a property value expected to return a string.
|
||||
|
||||
@@ -13,12 +13,12 @@ class ManyThings:
|
||||
)
|
||||
|
||||
def other_things(self):
|
||||
self.vm.bus[3].gain = -6.3
|
||||
self.vm.bus[4].eq = True
|
||||
info = (
|
||||
f"bus 3 gain has been set to {self.vm.bus[3].gain}",
|
||||
f"bus 4 eq has been set to {self.vm.bus[4].eq}",
|
||||
)
|
||||
self.vm.bus[3].gain = -6.3
|
||||
self.vm.bus[4].eq = True
|
||||
print("\n".join(info))
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import argparse
|
||||
import logging
|
||||
import time
|
||||
|
||||
import voicemeeterlib
|
||||
@@ -13,6 +15,11 @@ from pyparsing import (
|
||||
nums,
|
||||
)
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
argparser = argparse.ArgumentParser(description="creates a basic dsl")
|
||||
argparser.add_argument("-i", action="store_true")
|
||||
args = argparser.parse_args()
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, vm):
|
||||
@@ -53,46 +60,39 @@ class Parser:
|
||||
return res
|
||||
|
||||
|
||||
def main(cmds=None):
|
||||
kind_id = "banana"
|
||||
|
||||
with voicemeeterlib.api(kind_id) as vm:
|
||||
parser = Parser(vm)
|
||||
if cmds:
|
||||
res = parser.parse(cmds)
|
||||
if res:
|
||||
print(res)
|
||||
else:
|
||||
def interactive_mode(parser):
|
||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||
if not cmd:
|
||||
break
|
||||
res = parser.parse((cmd,))
|
||||
if res:
|
||||
if res := parser.parse((cmd,)):
|
||||
print(res)
|
||||
|
||||
|
||||
def main():
|
||||
# fmt: off
|
||||
cmds = (
|
||||
"strip 0 -> mute -> on", "strip 0 -> mute", "bus 0 -> mute -> on",
|
||||
"strip 0 -> mute -> off", "bus 0 -> mute -> on", "strip 3 -> solo -> on",
|
||||
"strip 3 -> solo -> off", "strip 1 -> A1 -> on", "strip 1 -> A1",
|
||||
"strip 1 -> A1 -> off", "strip 1 -> A1", "bus 3 -> eq -> on",
|
||||
"bus 3 -> eq -> off", "strip 4 -> gain -> 1.2", "strip 0 -> gain -> -8.2",
|
||||
"strip 0 -> gain", "strip 1 -> label -> rode podmic", "strip 2 -> limit -> -28",
|
||||
"strip 2 -> limit",
|
||||
)
|
||||
# fmt: on
|
||||
|
||||
kind_id = "banana"
|
||||
subs = {ev: False for ev in ["pdirty", "mdirty", "midi"]}
|
||||
|
||||
with voicemeeterlib.api(kind_id, subs=subs) as vm:
|
||||
parser = Parser(vm)
|
||||
if args.i:
|
||||
interactive_mode(parser)
|
||||
return
|
||||
|
||||
if res := parser.parse(cmds):
|
||||
print(res)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
cmds = (
|
||||
"strip 0 -> mute -> on",
|
||||
"strip 0 -> mute",
|
||||
"bus 0 -> mute -> on",
|
||||
"strip 0 -> mute -> off",
|
||||
"bus 0 -> mute -> on",
|
||||
"strip 3 -> solo -> on",
|
||||
"strip 3 -> solo -> off",
|
||||
"strip 1 -> A1 -> on",
|
||||
"strip 1 -> A1",
|
||||
"strip 1 -> A1 -> off",
|
||||
"strip 1 -> A1",
|
||||
"bus 3 -> eq -> on",
|
||||
"bus 3 -> eq -> off",
|
||||
"strip 4 -> gain -> 1.2",
|
||||
"strip 0 -> gain -> -8.2",
|
||||
"strip 0 -> gain",
|
||||
"strip 1 -> label -> rode podmic",
|
||||
"strip 2 -> limit -> -28",
|
||||
"strip 2 -> limit",
|
||||
)
|
||||
|
||||
# pass cmds to parse cmds, otherwise simply run main() to test stdin parsing
|
||||
main(cmds)
|
||||
main()
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import logging
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vm, midi_btn, macrobutton):
|
||||
self.vm = vm
|
||||
self.midi_btn = midi_btn
|
||||
self.macrobutton = macrobutton
|
||||
# leftmost M on korg nanokontrol2 in CC mode
|
||||
MIDI_BUTTON = 48
|
||||
MACROBUTTON = 0
|
||||
|
||||
def register(self):
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
self.vm.subject.add(self)
|
||||
|
||||
def on_update(self, subject):
|
||||
@@ -32,23 +36,24 @@ class Observer:
|
||||
"""
|
||||
if (
|
||||
max(self.vm.strip[3].levels.postfader) > -40
|
||||
and self.vm.midi.get(self.midi_btn) == 127
|
||||
and self.vm.midi.get(self.MIDI_BUTTON) == 127
|
||||
):
|
||||
print(
|
||||
f"Strip 3 level is greater than -40 and midi button {self.midi_btn} is pressed"
|
||||
f"Strip 3 level 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].state = False
|
||||
self.vm.button[self.MACROBUTTON].trigger = False
|
||||
self.vm.button[self.MACROBUTTON].state = False
|
||||
|
||||
|
||||
def main():
|
||||
kind_id = "banana"
|
||||
|
||||
# we only care about midi events here.
|
||||
subs = {ev: False for ev in ["pdirty", "mdirty", "ldirty"]}
|
||||
subs = {ev: False for ev in ["pdirty", "mdirty"]}
|
||||
with voicemeeterlib.api(kind_id, subs=subs) as vm:
|
||||
obs = Observer(vm, midi_btn, macrobutton)
|
||||
obs.register()
|
||||
obs = Observer(vm)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
@@ -56,9 +61,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
# leftmost M on korg nanokontrol2 in CC mode
|
||||
midi_btn = 48
|
||||
macrobutton = 0
|
||||
|
||||
main()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
## Requirements
|
||||
|
||||
- [OBS Studio](https://obsproject.com/)
|
||||
- [OBS Websocket v5 Plugin](https://github.com/obsproject/obs-websocket/releases/tag/5.0.0)
|
||||
- [OBS Python SDK for Websocket v5](https://github.com/aatikturk/obsstudio_sdk)
|
||||
- [OBS Python SDK for Websocket v5](https://github.com/aatikturk/obsws-python)
|
||||
|
||||
## About
|
||||
|
||||
@@ -20,6 +19,6 @@ password = "mystrongpass"
|
||||
|
||||
## Notes
|
||||
|
||||
For a similar example for streamlabs check:
|
||||
For a similar Streamlabs Desktop example:
|
||||
|
||||
[Streamlabs example](https://github.com/onyx-and-iris/PySLOBS/blob/master/examples/scenerotate.py)
|
||||
[Streamlabs example](https://gist.github.com/onyx-and-iris/c864f07126eeae389b011dc49520a19b)
|
||||
|
||||
@@ -1,20 +1,28 @@
|
||||
import obsstudio_sdk as obs
|
||||
import logging
|
||||
|
||||
import obsws_python as obs
|
||||
import voicemeeterlib
|
||||
|
||||
|
||||
def on_start():
|
||||
vm.strip[0].mute = True
|
||||
vm.strip[1].B1 = True
|
||||
vm.strip[2].B2 = True
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
def on_brb():
|
||||
vm.strip[7].fadeto(0, 500)
|
||||
vm.bus[0].mute = True
|
||||
class Observer:
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
self.client = obs.EventClient()
|
||||
self.client.callback.register(self.on_current_program_scene_changed)
|
||||
|
||||
def on_start(self):
|
||||
self.vm.strip[0].mute = True
|
||||
self.vm.strip[1].B1 = True
|
||||
self.vm.strip[2].B2 = True
|
||||
|
||||
def on_end():
|
||||
vm.apply(
|
||||
def on_brb(self):
|
||||
self.vm.strip[7].fadeto(0, 500)
|
||||
self.vm.bus[0].mute = True
|
||||
|
||||
def on_end(self):
|
||||
self.vm.apply(
|
||||
{
|
||||
"strip-0": {"mute": True},
|
||||
"strip-1": {"mute": True, "B1": False},
|
||||
@@ -23,36 +31,36 @@ def on_end():
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
def on_live():
|
||||
vm.strip[0].mute = False
|
||||
vm.strip[7].fadeto(-6, 500)
|
||||
vm.strip[7].A3 = True
|
||||
vm.vban.instream[0].on = True
|
||||
def on_current_program_scene_changed(self, data):
|
||||
def fget(scene):
|
||||
run = {
|
||||
"START": self.on_start,
|
||||
"BRB": self.on_brb,
|
||||
"END": self.on_end,
|
||||
"LIVE": self.on_live,
|
||||
}
|
||||
return run.get(scene)
|
||||
|
||||
|
||||
def on_current_program_scene_changed(data):
|
||||
scene = data.scene_name
|
||||
print(f"Switched to scene {scene}")
|
||||
|
||||
match scene:
|
||||
case "START":
|
||||
on_start()
|
||||
case "BRB":
|
||||
on_brb()
|
||||
case "END":
|
||||
on_end()
|
||||
case "LIVE":
|
||||
on_live()
|
||||
case _:
|
||||
pass
|
||||
if fn := fget(scene):
|
||||
fn()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with voicemeeterlib.api("potato") as vm:
|
||||
cl = obs.EventClient()
|
||||
cl.callback.register(on_current_program_scene_changed)
|
||||
|
||||
def main():
|
||||
subs = {ev: False for ev in ["pdirty", "mdirty", "midi"]}
|
||||
with voicemeeterlib.api("potato", subs=subs) as vm:
|
||||
obs = Observer(vm)
|
||||
while cmd := input("<Enter> to exit\n"):
|
||||
if not cmd:
|
||||
break
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -3,5 +3,5 @@ from setuptools import setup
|
||||
setup(
|
||||
name="obs",
|
||||
description="OBS Example",
|
||||
install_requires=["voicemeeter-api", "obsstudio-sdk"],
|
||||
install_requires=["voicemeeter-api", "obsws-python"],
|
||||
)
|
||||
|
||||
14
examples/observer/README.md
Normal file
14
examples/observer/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
## About
|
||||
|
||||
Registers a class as an observer and defines a callback.
|
||||
|
||||
## Use
|
||||
|
||||
Run the script, then:
|
||||
|
||||
- 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
|
||||
|
||||
Pressing `<Enter>` will exit.
|
||||
@@ -1,13 +1,17 @@
|
||||
import logging
|
||||
|
||||
import voicemeeterlib
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class Observer:
|
||||
def __init__(self, vm):
|
||||
self.vm = vm
|
||||
# register your app as event observer
|
||||
self.vm.subject.add(self)
|
||||
# add level updates, since they are disabled by default.
|
||||
self.vm.event.add("ldirty")
|
||||
# enable level updates, since they are disabled by default.
|
||||
self.vm.event.ldirty = True
|
||||
|
||||
# define an 'on_update' callback function to receive event updates
|
||||
def on_update(self, subject):
|
||||
@@ -16,22 +20,19 @@ class Observer:
|
||||
elif subject == "mdirty":
|
||||
print("mdirty!")
|
||||
elif subject == "ldirty":
|
||||
info = (
|
||||
f"[{self.vm.bus[0]} {self.vm.bus[0].levels.isdirty}]",
|
||||
f"[{self.vm.bus[1]} {self.vm.bus[1].levels.isdirty}]",
|
||||
f"[{self.vm.bus[2]} {self.vm.bus[2].levels.isdirty}]",
|
||||
f"[{self.vm.bus[3]} {self.vm.bus[3].levels.isdirty}]",
|
||||
f"[{self.vm.bus[4]} {self.vm.bus[4].levels.isdirty}]",
|
||||
)
|
||||
print(" ".join(info))
|
||||
for bus in self.vm.bus:
|
||||
if bus.levels.isdirty:
|
||||
print(bus, bus.levels.all)
|
||||
elif subject == "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) as vm:
|
||||
obs = Observer(vm)
|
||||
Observer(vm)
|
||||
|
||||
while cmd := input("Press <Enter> to exit\n"):
|
||||
if not cmd:
|
||||
@@ -39,6 +40,4 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
kind_id = "banana"
|
||||
|
||||
main()
|
||||
|
||||
125
poetry.lock
generated
125
poetry.lock
generated
@@ -1,11 +1,3 @@
|
||||
[[package]]
|
||||
name = "atomicwrites"
|
||||
version = "1.4.1"
|
||||
description = "Atomic file writes."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||
|
||||
[[package]]
|
||||
name = "attrs"
|
||||
version = "22.1.0"
|
||||
@@ -22,7 +14,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
||||
|
||||
[[package]]
|
||||
name = "black"
|
||||
version = "22.6.0"
|
||||
version = "22.8.0"
|
||||
description = "The uncompromising code formatter."
|
||||
category = "dev"
|
||||
optional = false
|
||||
@@ -33,6 +25,7 @@ click = ">=8.0.0"
|
||||
mypy-extensions = ">=0.4.3"
|
||||
pathspec = ">=0.9.0"
|
||||
platformdirs = ">=2"
|
||||
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||
|
||||
[package.extras]
|
||||
colorama = ["colorama (>=0.4.3)"]
|
||||
@@ -102,11 +95,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||
|
||||
[[package]]
|
||||
name = "pathspec"
|
||||
version = "0.9.0"
|
||||
version = "0.10.1"
|
||||
description = "Utility library for gitignore style pattern matching of file paths."
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
@@ -129,8 +122,8 @@ optional = false
|
||||
python-versions = ">=3.6"
|
||||
|
||||
[package.extras]
|
||||
dev = ["pre-commit", "tox"]
|
||||
testing = ["pytest", "pytest-benchmark"]
|
||||
testing = ["pytest-benchmark", "pytest"]
|
||||
dev = ["tox", "pre-commit"]
|
||||
|
||||
[[package]]
|
||||
name = "py"
|
||||
@@ -153,14 +146,13 @@ diagrams = ["railroad-diagrams", "jinja2"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "7.1.2"
|
||||
version = "7.1.3"
|
||||
description = "pytest: simple powerful testing with Python"
|
||||
category = "dev"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[package.dependencies]
|
||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||
attrs = ">=19.2.0"
|
||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
iniconfig = "*"
|
||||
@@ -198,97 +190,30 @@ pytest = ">=3.6"
|
||||
name = "tomli"
|
||||
version = "2.0.1"
|
||||
description = "A lil' TOML parser"
|
||||
category = "dev"
|
||||
category = "main"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
|
||||
[metadata]
|
||||
lock-version = "1.1"
|
||||
python-versions = "^3.11"
|
||||
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014"
|
||||
python-versions = "^3.10"
|
||||
content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
|
||||
|
||||
[metadata.files]
|
||||
atomicwrites = []
|
||||
attrs = []
|
||||
black = [
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
|
||||
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
|
||||
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
|
||||
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
|
||||
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
|
||||
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
|
||||
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
|
||||
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
|
||||
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
|
||||
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
|
||||
{file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"},
|
||||
{file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"},
|
||||
{file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"},
|
||||
{file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"},
|
||||
{file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"},
|
||||
{file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"},
|
||||
{file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"},
|
||||
{file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"},
|
||||
]
|
||||
click = [
|
||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||
]
|
||||
colorama = [
|
||||
{file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"},
|
||||
{file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"},
|
||||
]
|
||||
iniconfig = [
|
||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||
]
|
||||
isort = [
|
||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
||||
]
|
||||
mypy-extensions = [
|
||||
{file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
|
||||
{file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
|
||||
]
|
||||
packaging = [
|
||||
{file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
|
||||
{file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
|
||||
]
|
||||
pathspec = [
|
||||
{file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
|
||||
{file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
|
||||
]
|
||||
platformdirs = [
|
||||
{file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"},
|
||||
{file = "platformdirs-2.5.2.tar.gz", hash = "sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"},
|
||||
]
|
||||
pluggy = [
|
||||
{file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
|
||||
{file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
|
||||
]
|
||||
py = [
|
||||
{file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
|
||||
{file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
|
||||
]
|
||||
pyparsing = [
|
||||
{file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"},
|
||||
{file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"},
|
||||
]
|
||||
pytest = [
|
||||
{file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"},
|
||||
{file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"},
|
||||
]
|
||||
pytest-randomly = [
|
||||
{file = "pytest-randomly-3.12.0.tar.gz", hash = "sha256:d60c2db71ac319aee0fc6c4110a7597d611a8b94a5590918bfa8583f00caccb2"},
|
||||
{file = "pytest_randomly-3.12.0-py3-none-any.whl", hash = "sha256:f4f2e803daf5d1ba036cc22bf4fe9dbbf99389ec56b00e5cba732fb5c1d07fdd"},
|
||||
]
|
||||
black = []
|
||||
click = []
|
||||
colorama = []
|
||||
iniconfig = []
|
||||
isort = []
|
||||
mypy-extensions = []
|
||||
packaging = []
|
||||
pathspec = []
|
||||
platformdirs = []
|
||||
pluggy = []
|
||||
py = []
|
||||
pyparsing = []
|
||||
pytest = []
|
||||
pytest-randomly = []
|
||||
pytest-repeat = []
|
||||
tomli = [
|
||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||
]
|
||||
tomli = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "voicemeeter-api"
|
||||
version = "0.6.0"
|
||||
version = "1.0.0"
|
||||
description = "A Python wrapper for the Voiceemeter API"
|
||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||
license = "MIT"
|
||||
@@ -12,7 +12,8 @@ packages = [
|
||||
]
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.11"
|
||||
python = "^3.10"
|
||||
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||
|
||||
[tool.poetry.dev-dependencies]
|
||||
pytest = "^7.1.2"
|
||||
@@ -24,3 +25,10 @@ isort = "^5.10.1"
|
||||
[build-system]
|
||||
requires = ["poetry-core>=1.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
dsl = "scripts:ex_dsl"
|
||||
midi = "scripts:ex_midi"
|
||||
obs = "scripts:ex_obs"
|
||||
observer = "scripts:ex_observer"
|
||||
test ="scripts:test"
|
||||
|
||||
26
scripts.py
Normal file
26
scripts.py
Normal file
@@ -0,0 +1,26 @@
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def ex_dsl():
|
||||
path = Path.cwd() / "examples" / "dsl" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def ex_midi():
|
||||
path = Path.cwd() / "examples" / "midi" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def ex_obs():
|
||||
path = Path.cwd() / "examples" / "obs" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def ex_observer():
|
||||
path = Path.cwd() / "examples" / "observer" / "."
|
||||
subprocess.run(["py", str(path)])
|
||||
|
||||
|
||||
def test():
|
||||
subprocess.run(["pytest", "-v"])
|
||||
@@ -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: 114"><title>tests: 114</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">114</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">114</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: 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>
|
||||
|
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: 156"><title>tests: 156</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">156</text><text x="515" y="140" transform="scale(.1)" fill="#fff" textLength="210">156</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 |
@@ -381,6 +381,8 @@ class TestSetAndGetFloatHigher:
|
||||
@pytest.mark.parametrize(
|
||||
"index, param, value",
|
||||
[
|
||||
(data.virt_in, "pan_x", -0.6),
|
||||
(data.virt_in, "pan_x", 0.6),
|
||||
(data.virt_in, "treble", -1.6),
|
||||
(data.virt_in, "mid", 5.8),
|
||||
(data.virt_in, "bass", -8.1),
|
||||
|
||||
@@ -4,10 +4,9 @@ from enum import IntEnum
|
||||
from math import log
|
||||
from typing import Union
|
||||
|
||||
from .error import VMError
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
from .meta import bool_prop, bus_mode_prop, float_prop
|
||||
from .meta import bus_mode_prop, device_prop, float_prop
|
||||
|
||||
BusModes = IntEnum(
|
||||
"BusModes",
|
||||
@@ -106,7 +105,7 @@ class Bus(IRemote):
|
||||
|
||||
class PhysicalBus(Bus):
|
||||
@classmethod
|
||||
def make(cls, kind):
|
||||
def make(cls, remote, i, kind):
|
||||
"""
|
||||
Factory method for PhysicalBus.
|
||||
|
||||
@@ -116,18 +115,54 @@ class PhysicalBus(Bus):
|
||||
if kind.name == "potato":
|
||||
EFFECTS_cls = _make_effects_mixin()
|
||||
kls += (EFFECTS_cls,)
|
||||
return type("PhysicalBus", kls, {})
|
||||
return type(
|
||||
"PhysicalBus",
|
||||
kls,
|
||||
{
|
||||
"device": BusDevice.make(remote, i),
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
|
||||
class BusDevice(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i):
|
||||
"""
|
||||
Factory function for bus.device.
|
||||
|
||||
Returns a BusDevice class of a kind.
|
||||
"""
|
||||
DEVICE_cls = type(
|
||||
f"BusDevice{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
param: device_prop(param)
|
||||
for param in [
|
||||
"wdm",
|
||||
"ks",
|
||||
"mme",
|
||||
"asio",
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
return DEVICE_cls(remote, i)
|
||||
|
||||
@property
|
||||
def device(self) -> str:
|
||||
return self.getter("device.name", is_string=True)
|
||||
def identifier(self) -> str:
|
||||
return f"Bus[{self.index}].device"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.getter("name", is_string=True)
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return int(self.getter("device.sr"))
|
||||
return int(self.getter("sr"))
|
||||
|
||||
|
||||
class VirtualBus(Bus):
|
||||
@@ -263,7 +298,9 @@ def bus_factory(is_phys_bus, remote, i) -> Union[PhysicalBus, VirtualBus]:
|
||||
Returns a physical or virtual bus subclass
|
||||
"""
|
||||
BUS_cls = (
|
||||
PhysicalBus.make(remote.kind) if is_phys_bus else VirtualBus.make(remote.kind)
|
||||
PhysicalBus.make(remote, i, remote.kind)
|
||||
if is_phys_bus
|
||||
else VirtualBus.make(remote.kind)
|
||||
)
|
||||
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||
return type(
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import itertools
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import tomllib
|
||||
try:
|
||||
import tomllib
|
||||
except ModuleNotFoundError:
|
||||
import tomli as tomllib
|
||||
|
||||
from .kinds import request_kind_map as kindmap
|
||||
|
||||
@@ -70,7 +74,6 @@ class TOMLStrBuilder:
|
||||
|
||||
class TOMLDataExtractor:
|
||||
def __init__(self, file):
|
||||
self._data = dict()
|
||||
with open(file, "rb") as f:
|
||||
self._data = tomllib.load(f)
|
||||
|
||||
@@ -116,6 +119,8 @@ class Loader(metaclass=SingletonType):
|
||||
loads data into memory if not found
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("config.Loader")
|
||||
|
||||
def __init__(self, kind):
|
||||
self._kind = kind
|
||||
self._configs = dict()
|
||||
@@ -129,14 +134,16 @@ class Loader(metaclass=SingletonType):
|
||||
|
||||
def parse(self, identifier, data):
|
||||
if identifier in self._configs:
|
||||
print(f"config file with name {identifier} already in memory, skipping..")
|
||||
self.logger.info(
|
||||
f"config file with name {identifier} already in memory, skipping.."
|
||||
)
|
||||
return False
|
||||
self.parser = dataextraction_factory(data)
|
||||
return True
|
||||
|
||||
def register(self, identifier, data=None):
|
||||
self._configs[identifier] = data if data else self.parser.data
|
||||
print(f"config {self.name}/{identifier} loaded into memory")
|
||||
self.logger.info(f"config {self.name}/{identifier} loaded into memory")
|
||||
|
||||
def deregister(self):
|
||||
self._configs.clear()
|
||||
@@ -159,6 +166,7 @@ def loader(kind):
|
||||
|
||||
returns configs loaded into memory
|
||||
"""
|
||||
logger = logging.getLogger("config.loader")
|
||||
loader = Loader(kind)
|
||||
|
||||
for path in (
|
||||
@@ -167,7 +175,7 @@ def loader(kind):
|
||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||
):
|
||||
if path.is_dir():
|
||||
print(f"Checking [{path}] for TOML config files:")
|
||||
logger.info(f"Checking [{path}] for TOML config files:")
|
||||
for file in path.glob("*.toml"):
|
||||
identifier = file.with_suffix("").stem
|
||||
if loader.parse(identifier, file):
|
||||
|
||||
73
voicemeeterlib/event.py
Normal file
73
voicemeeterlib/event.py
Normal file
@@ -0,0 +1,73 @@
|
||||
import logging
|
||||
from typing import Iterable, Union
|
||||
|
||||
|
||||
class Event:
|
||||
"""Keeps track of event subscriptions"""
|
||||
|
||||
logger = logging.getLogger("event.event")
|
||||
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
|
||||
def info(self, msg=None):
|
||||
info = (f"{msg} events",) if msg else ()
|
||||
if self.any():
|
||||
info += (f"now listening for {', '.join(self.get())} events",)
|
||||
else:
|
||||
info += (f"not listening for any events",)
|
||||
self.logger.info(", ".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self) -> bool:
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@pdirty.setter
|
||||
def pdirty(self, val: bool):
|
||||
self.subs["pdirty"] = val
|
||||
self.info(f"pdirty {'added to' if val else 'removed from'}")
|
||||
|
||||
@property
|
||||
def mdirty(self) -> bool:
|
||||
return self.subs["mdirty"]
|
||||
|
||||
@mdirty.setter
|
||||
def mdirty(self, val: bool):
|
||||
self.subs["mdirty"] = val
|
||||
self.info(f"mdirty {'added to' if val else 'removed from'}")
|
||||
|
||||
@property
|
||||
def midi(self) -> bool:
|
||||
return self.subs["midi"]
|
||||
|
||||
@midi.setter
|
||||
def midi(self, val: bool):
|
||||
self.subs["midi"] = val
|
||||
self.info(f"midi {'added to' if val else 'removed from'}")
|
||||
|
||||
@property
|
||||
def ldirty(self) -> bool:
|
||||
return self.subs["ldirty"]
|
||||
|
||||
@ldirty.setter
|
||||
def ldirty(self, val: bool):
|
||||
self.subs["ldirty"] = val
|
||||
self.info(f"ldirty {'added to' if val else 'removed from'}")
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, True)
|
||||
|
||||
def remove(self, events: Union[str, Iterable[str]]):
|
||||
if isinstance(events, str):
|
||||
events = [events]
|
||||
for event in events:
|
||||
setattr(self, event, False)
|
||||
@@ -1,10 +1,10 @@
|
||||
import logging
|
||||
from abc import abstractmethod
|
||||
from enum import IntEnum
|
||||
from functools import cached_property
|
||||
from typing import Iterable, NoReturn, Self
|
||||
from typing import Iterable, NoReturn
|
||||
|
||||
from . import misc
|
||||
from .base import Remote
|
||||
from .bus import request_bus_obj as bus
|
||||
from .command import Command
|
||||
from .config import request_config as configs
|
||||
@@ -13,6 +13,7 @@ from .kinds import KindMapClass
|
||||
from .kinds import request_kind_map as kindmap
|
||||
from .macrobutton import MacroButton
|
||||
from .recorder import Recorder
|
||||
from .remote import Remote
|
||||
from .strip import request_strip_obj as strip
|
||||
from .vban import request_vban_obj as vban
|
||||
|
||||
@@ -24,6 +25,7 @@ class FactoryBuilder:
|
||||
Separates construction from representation.
|
||||
"""
|
||||
|
||||
logger = logging.getLogger("remote.factorybuilder")
|
||||
BuilderProgress = IntEnum(
|
||||
"BuilderProgress",
|
||||
"strip bus command macrobutton vban device option recorder patch fx",
|
||||
@@ -49,51 +51,51 @@ class FactoryBuilder:
|
||||
def _pinfo(self, name: str) -> NoReturn:
|
||||
"""prints progress status for each step"""
|
||||
name = name.split("_")[1]
|
||||
print(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
self.logger.info(self._info[int(getattr(self.BuilderProgress, name))])
|
||||
|
||||
def make_strip(self) -> Self:
|
||||
def make_strip(self):
|
||||
self._factory.strip = tuple(
|
||||
strip(i < self.kind.phys_in, self._factory, i)
|
||||
for i in range(self.kind.num_strip)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_bus(self) -> Self:
|
||||
def make_bus(self):
|
||||
self._factory.bus = tuple(
|
||||
bus(i < self.kind.phys_out, self._factory, i)
|
||||
for i in range(self.kind.num_bus)
|
||||
)
|
||||
return self
|
||||
|
||||
def make_command(self) -> Self:
|
||||
def make_command(self):
|
||||
self._factory.command = Command.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_macrobutton(self) -> 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:
|
||||
def make_vban(self):
|
||||
self._factory.vban = vban(self._factory)
|
||||
return self
|
||||
|
||||
def make_device(self) -> Self:
|
||||
def make_device(self):
|
||||
self._factory.device = Device.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_option(self) -> Self:
|
||||
def make_option(self):
|
||||
self._factory.option = misc.Option.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_recorder(self) -> Self:
|
||||
def make_recorder(self):
|
||||
self._factory.recorder = Recorder.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_patch(self) -> Self:
|
||||
def make_patch(self):
|
||||
self._factory.patch = misc.Patch.make(self._factory)
|
||||
return self
|
||||
|
||||
def make_fx(self) -> Self:
|
||||
def make_fx(self):
|
||||
self._factory.fx = misc.FX(self._factory)
|
||||
return self
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import time
|
||||
from abc import ABCMeta, abstractmethod
|
||||
from typing import Self
|
||||
|
||||
|
||||
class IRemote(metaclass=ABCMeta):
|
||||
@@ -26,7 +25,7 @@ class IRemote(metaclass=ABCMeta):
|
||||
def identifier(self):
|
||||
pass
|
||||
|
||||
def apply(self, data: dict) -> Self:
|
||||
def apply(self, data: dict):
|
||||
def fget(attr, val):
|
||||
if attr == "mode":
|
||||
return (getattr(self, attr), val, 1)
|
||||
|
||||
@@ -42,3 +42,12 @@ def bus_mode_prop(param):
|
||||
self.setter(param, 1 if val else 0)
|
||||
|
||||
return property(fget, fset)
|
||||
|
||||
|
||||
def device_prop(param):
|
||||
"""meta function for strip device parameters"""
|
||||
|
||||
def fset(self, val: str):
|
||||
self.setter(param, val)
|
||||
|
||||
return property(fset=fset)
|
||||
|
||||
@@ -250,45 +250,3 @@ class Midi:
|
||||
|
||||
def _set(self, key: int, velocity: int):
|
||||
self.cache[key] = velocity
|
||||
|
||||
|
||||
class Event:
|
||||
def __init__(self, subs: dict):
|
||||
self.subs = subs
|
||||
|
||||
def info(self, msg):
|
||||
info = (
|
||||
f"{msg} events",
|
||||
f"Now listening for {', '.join(self.get())} events",
|
||||
)
|
||||
print("\n".join(info))
|
||||
|
||||
@property
|
||||
def pdirty(self):
|
||||
return self.subs["pdirty"]
|
||||
|
||||
@property
|
||||
def mdirty(self):
|
||||
return self.subs["mdirty"]
|
||||
|
||||
@property
|
||||
def midi(self):
|
||||
return self.subs["midi"]
|
||||
|
||||
@property
|
||||
def ldirty(self):
|
||||
return self.subs["ldirty"]
|
||||
|
||||
def get(self) -> list:
|
||||
return [k for k, v in self.subs.items() if v]
|
||||
|
||||
def any(self) -> bool:
|
||||
return any(self.subs.values())
|
||||
|
||||
def add(self, event):
|
||||
self.subs[event] = True
|
||||
self.info(f"{event} added to")
|
||||
|
||||
def remove(self, event):
|
||||
self.subs[event] = False
|
||||
self.info(f"{event} removed from")
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import ctypes as ct
|
||||
import logging
|
||||
import time
|
||||
from abc import abstractmethod
|
||||
from functools import partial
|
||||
from threading import Thread
|
||||
from typing import Iterable, NoReturn, Optional, Self, Union
|
||||
from typing import Iterable, NoReturn, Optional, Union
|
||||
|
||||
from .cbindings import CBindings
|
||||
from .error import CAPIError, VMError
|
||||
from .event import Event
|
||||
from .inst import bits
|
||||
from .kinds import KindId
|
||||
from .misc import Event, Midi
|
||||
from .misc import Midi
|
||||
from .subject import Subject
|
||||
from .util import comp, grouper, polling, script
|
||||
from .updater import Updater
|
||||
from .util import grouper, polling, script
|
||||
|
||||
|
||||
class Remote(CBindings):
|
||||
"""Base class responsible for wrapping the C Remote API"""
|
||||
|
||||
logger = logging.getLogger("remote.remote")
|
||||
DELAY = 0.001
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
@@ -32,7 +35,7 @@ class Remote(CBindings):
|
||||
|
||||
self.event = Event(self.subs)
|
||||
|
||||
def __enter__(self) -> Self:
|
||||
def __enter__(self):
|
||||
"""setup procedures"""
|
||||
self.login()
|
||||
self.init_thread()
|
||||
@@ -46,37 +49,10 @@ class Remote(CBindings):
|
||||
def init_thread(self):
|
||||
"""Starts updates thread."""
|
||||
self.running = True
|
||||
print(f"Listening for {', '.join(self.event.get())} events")
|
||||
t = Thread(target=self._updates, daemon=True)
|
||||
t.start()
|
||||
self.event.info()
|
||||
|
||||
def _updates(self):
|
||||
"""
|
||||
Continously update observers of dirty states.
|
||||
|
||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||
|
||||
Runs updates at a rate of self.ratelimit.
|
||||
"""
|
||||
while self.running:
|
||||
if self.event.pdirty and self.pdirty:
|
||||
self.subject.notify("pdirty")
|
||||
if self.event.mdirty and self.mdirty:
|
||||
self.subject.notify("mdirty")
|
||||
if self.event.midi and self.get_midi_message():
|
||||
self.subject.notify("midi")
|
||||
if self.event.ldirty and self.ldirty:
|
||||
self._strip_comp, self._bus_comp = (
|
||||
tuple(
|
||||
not x for x in comp(self.cache["strip_level"], self._strip_buf)
|
||||
),
|
||||
tuple(not x for x in comp(self.cache["bus_level"], self._bus_buf)),
|
||||
)
|
||||
self.cache["strip_level"] = self._strip_buf
|
||||
self.cache["bus_level"] = self._bus_buf
|
||||
self.subject.notify("ldirty")
|
||||
|
||||
time.sleep(self.ratelimit if self.event.any() else 0.5)
|
||||
self.updater = Updater(self)
|
||||
self.updater.start()
|
||||
|
||||
def login(self) -> NoReturn:
|
||||
"""Login to the API, initialize dirty parameters"""
|
||||
@@ -85,7 +61,7 @@ class Remote(CBindings):
|
||||
self.run_voicemeeter(self.kind.name)
|
||||
elif res != 0:
|
||||
raise CAPIError(f"VBVMR_Login returned {res}")
|
||||
print(f"Successfully logged into {self}")
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged into {self}")
|
||||
self.clear_dirty()
|
||||
|
||||
def run_voicemeeter(self, kind_id: str) -> NoReturn:
|
||||
@@ -242,7 +218,9 @@ class Remote(CBindings):
|
||||
buf = ct.create_string_buffer(1024)
|
||||
res = self.vm_get_midi_message(ct.byref(buf), n)
|
||||
if res > 0:
|
||||
vals = tuple(grouper(3, (int.from_bytes(buf[i]) for i in range(res))))
|
||||
vals = tuple(
|
||||
grouper(3, (int.from_bytes(buf[i], "little") for i in range(res)))
|
||||
)
|
||||
for msg in vals:
|
||||
ch, pitch, vel = msg
|
||||
if not self.midi._channel or self.midi._channel != ch:
|
||||
@@ -287,9 +265,9 @@ class Remote(CBindings):
|
||||
)
|
||||
try:
|
||||
self.apply(self.configs[name])
|
||||
print(f"Profile '{name}' applied!")
|
||||
self.logger.info(f"Profile '{name}' applied!")
|
||||
except KeyError as e:
|
||||
print(("\n").join(error_msg))
|
||||
self.logger.error(("\n").join(error_msg))
|
||||
|
||||
def logout(self) -> NoReturn:
|
||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
||||
@@ -298,7 +276,7 @@ class Remote(CBindings):
|
||||
res = self.vm_logout()
|
||||
if res != 0:
|
||||
raise CAPIError(f"VBVMR_Logout returned {res}")
|
||||
print(f"Successfully logged out of {self}")
|
||||
self.logger.info(f"{type(self).__name__}: Successfully logged out of {self}")
|
||||
|
||||
def end_thread(self):
|
||||
self.running = False
|
||||
@@ -5,7 +5,7 @@ from typing import Union
|
||||
|
||||
from .iremote import IRemote
|
||||
from .kinds import kinds_all
|
||||
from .meta import bool_prop, float_prop
|
||||
from .meta import bool_prop, device_prop, float_prop
|
||||
|
||||
|
||||
class Strip(IRemote):
|
||||
@@ -82,14 +82,20 @@ class Strip(IRemote):
|
||||
|
||||
class PhysicalStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, kind):
|
||||
def make(cls, remote, i, is_phys):
|
||||
"""
|
||||
Factory method for PhysicalStrip.
|
||||
|
||||
Returns a PhysicalStrip class.
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins[kind.name]
|
||||
return type(f"PhysicalStrip", (cls, EFFECTS_cls), {})
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
f"PhysicalStrip",
|
||||
(cls, EFFECTS_cls),
|
||||
{
|
||||
"device": StripDevice.make(remote, i),
|
||||
},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
@@ -118,16 +124,60 @@ class PhysicalStrip(Strip):
|
||||
def audibility(self, val: float):
|
||||
self.setter("audibility", val)
|
||||
|
||||
@property
|
||||
def device(self):
|
||||
return self.getter("device.name", is_string=True)
|
||||
|
||||
class StripDevice(IRemote):
|
||||
@classmethod
|
||||
def make(cls, remote, i):
|
||||
"""
|
||||
Factory function for strip.device.
|
||||
|
||||
Returns a StripDevice class of a kind.
|
||||
"""
|
||||
DEVICE_cls = type(
|
||||
f"StripDevice{remote.kind}",
|
||||
(cls,),
|
||||
{
|
||||
**{
|
||||
param: device_prop(param)
|
||||
for param in [
|
||||
"wdm",
|
||||
"ks",
|
||||
"mme",
|
||||
"asio",
|
||||
]
|
||||
},
|
||||
},
|
||||
)
|
||||
return DEVICE_cls(remote, i)
|
||||
|
||||
@property
|
||||
def sr(self):
|
||||
return int(self.getter("device.sr"))
|
||||
def identifier(self) -> str:
|
||||
return f"Strip[{self.index}].device"
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return self.getter("name", is_string=True)
|
||||
|
||||
@property
|
||||
def sr(self) -> int:
|
||||
return int(self.getter("sr"))
|
||||
|
||||
|
||||
class VirtualStrip(Strip):
|
||||
@classmethod
|
||||
def make(cls, remote, i, is_phys):
|
||||
"""
|
||||
Factory method for VirtualStrip.
|
||||
|
||||
Returns a VirtualStrip class.
|
||||
"""
|
||||
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||
return type(
|
||||
f"VirtualStrip",
|
||||
(cls, EFFECTS_cls),
|
||||
{},
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{type(self).__name__}{self.index}"
|
||||
|
||||
@@ -304,36 +354,38 @@ _make_channelout_mixins = {
|
||||
}
|
||||
|
||||
|
||||
def _make_effects_mixin(kind):
|
||||
def _make_effects_mixin(kind, is_phys):
|
||||
"""creates an effects mixin for a kind"""
|
||||
XY_cls = type(
|
||||
"XY",
|
||||
|
||||
def _make_xy_cls():
|
||||
pan = {param: float_prop(param) for param in ["pan_x", "pan_y"]}
|
||||
color = {param: float_prop(param) for param in ["color_x", "color_y"]}
|
||||
fx = {param: float_prop(param) for param in ["fx_x", "fx_y"]}
|
||||
if is_phys:
|
||||
return type(
|
||||
"XYPhys",
|
||||
(),
|
||||
{
|
||||
param: float_prop(param)
|
||||
for param in [
|
||||
"pan_x",
|
||||
"pan_y",
|
||||
"color_x",
|
||||
"color_y",
|
||||
"fx_x",
|
||||
"fx_y",
|
||||
]
|
||||
**pan,
|
||||
**color,
|
||||
**fx,
|
||||
},
|
||||
)
|
||||
return type(
|
||||
"XYVirt",
|
||||
(),
|
||||
{**pan},
|
||||
)
|
||||
|
||||
FX_cls = type(
|
||||
def _make_fx_cls():
|
||||
if is_phys:
|
||||
return type(
|
||||
"FX",
|
||||
(),
|
||||
{
|
||||
**{
|
||||
param: float_prop(param)
|
||||
for param in [
|
||||
"reverb",
|
||||
"delay",
|
||||
"fx1",
|
||||
"fx2",
|
||||
]
|
||||
for param in ["reverb", "delay", "fx1", "fx2"]
|
||||
},
|
||||
**{
|
||||
f"post{param}": bool_prop(f"post{param}")
|
||||
@@ -341,13 +393,19 @@ def _make_effects_mixin(kind):
|
||||
},
|
||||
},
|
||||
)
|
||||
return type("FX", (), {})
|
||||
|
||||
if kind.name == "potato":
|
||||
return type(f"Effects{kind}", (XY_cls, FX_cls), {})
|
||||
return type(f"Effects{kind}", (XY_cls,), {})
|
||||
if kind.name == "basic":
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == "banana":
|
||||
steps = (_make_xy_cls,)
|
||||
elif kind.name == "potato":
|
||||
steps = (_make_xy_cls, _make_fx_cls)
|
||||
return type(f"Effects{kind}", tuple(step() for step in steps), {})
|
||||
|
||||
|
||||
_make_effects_mixins = {kind.name: _make_effects_mixin(kind) for kind in kinds_all}
|
||||
def _make_effects_mixins(is_phys):
|
||||
return {kind.name: _make_effects_mixin(kind, is_phys) for kind in kinds_all}
|
||||
|
||||
|
||||
def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip]:
|
||||
@@ -358,7 +416,11 @@ def strip_factory(is_phys_strip, remote, i) -> Union[PhysicalStrip, VirtualStrip
|
||||
|
||||
Returns a physical or virtual strip subclass
|
||||
"""
|
||||
STRIP_cls = PhysicalStrip.make(remote.kind) if is_phys_strip else VirtualStrip
|
||||
STRIP_cls = (
|
||||
PhysicalStrip.make(remote, i, is_phys_strip)
|
||||
if is_phys_strip
|
||||
else VirtualStrip.make(remote, i, is_phys_strip)
|
||||
)
|
||||
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||
|
||||
_kls = (STRIP_cls, CHANNELOUTMIXIN_cls)
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import logging
|
||||
|
||||
|
||||
class Subject:
|
||||
"""Adds support for observers"""
|
||||
|
||||
logger = logging.getLogger("subject.subject")
|
||||
|
||||
def __init__(self):
|
||||
"""list of current observers"""
|
||||
|
||||
@@ -22,16 +27,22 @@ class Subject:
|
||||
|
||||
if observer not in self._observers:
|
||||
self._observers.append(observer)
|
||||
self.logger.info(f"{type(observer).__name__} added to event observers")
|
||||
else:
|
||||
print(f"Failed to add: {observer}")
|
||||
self.logger.error(
|
||||
f"Failed to add {type(observer).__name__} to event observers"
|
||||
)
|
||||
|
||||
def remove(self, observer):
|
||||
"""removes an observer from _observers"""
|
||||
|
||||
try:
|
||||
self._observers.remove(observer)
|
||||
self.logger.info(f"{type(observer).__name__} removed from event observers")
|
||||
except ValueError:
|
||||
print(f"Failed to remove: {observer}")
|
||||
self.logger.error(
|
||||
f"Failed to remove {type(observer).__name__} from event observers"
|
||||
)
|
||||
|
||||
def clear(self):
|
||||
"""clears the _observers list"""
|
||||
|
||||
48
voicemeeterlib/updater.py
Normal file
48
voicemeeterlib/updater.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
from .util import comp
|
||||
|
||||
|
||||
class Updater(threading.Thread):
|
||||
def __init__(self, remote):
|
||||
super().__init__(name="updater", target=self.update, daemon=True)
|
||||
self._remote = remote
|
||||
self._remote._strip_comp = [False] * (
|
||||
2 * self._remote.kind.phys_in + 8 * self._remote.kind.virt_in
|
||||
)
|
||||
self._remote._bus_comp = [False] * (self._remote.kind.num_bus * 8)
|
||||
|
||||
def _update_comps(self, strip_level, bus_level):
|
||||
self._remote._strip_comp, self._remote._bus_comp = (
|
||||
tuple(not x for x in comp(self._remote.cache["strip_level"], strip_level)),
|
||||
tuple(not x for x in comp(self._remote.cache["bus_level"], bus_level)),
|
||||
)
|
||||
|
||||
def update(self):
|
||||
"""
|
||||
Continously update observers of dirty states.
|
||||
|
||||
Generate _strip_comp, _bus_comp and update level cache if ldirty.
|
||||
|
||||
Runs updates at a rate of self.ratelimit.
|
||||
"""
|
||||
while self._remote.running:
|
||||
start = time.time()
|
||||
if self._remote.event.pdirty and self._remote.pdirty:
|
||||
self._remote.subject.notify("pdirty")
|
||||
if self._remote.event.mdirty and self._remote.mdirty:
|
||||
self._remote.subject.notify("mdirty")
|
||||
if self._remote.event.midi and self._remote.get_midi_message():
|
||||
self._remote.subject.notify("midi")
|
||||
if self._remote.event.ldirty and self._remote.ldirty:
|
||||
self._update_comps(self._remote._strip_buf, self._remote._bus_buf)
|
||||
self._remote.cache["strip_level"] = self._remote._strip_buf
|
||||
self._remote.cache["bus_level"] = self._remote._bus_buf
|
||||
self._remote.subject.notify("ldirty")
|
||||
|
||||
elapsed = time.time() - start
|
||||
if self._remote.event.any() and self._remote.ratelimit - elapsed > 0:
|
||||
time.sleep(self._remote.ratelimit - elapsed)
|
||||
else:
|
||||
time.sleep(0.1)
|
||||
Reference in New Issue
Block a user