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
|
# test/config
|
||||||
quick.py
|
quick.py
|
||||||
config.toml
|
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]
|
- [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
|
## [0.6.0] - 2022-08-02
|
||||||
|
|
||||||
### Added
|
### 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
|
## Tested against
|
||||||
|
|
||||||
- Basic 1.0.8.2
|
- Basic 1.0.8.4
|
||||||
- Banana 2.0.6.2
|
- Banana 2.0.6.4
|
||||||
- Potato 3.0.2.2
|
- Potato 3.0.2.4
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [Voicemeeter](https://voicemeeter.com/)
|
- [Voicemeeter](https://voicemeeter.com/)
|
||||||
- Python 3.11 or greater
|
- Python 3.10 or greater
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
### `Pip`
|
|
||||||
|
|
||||||
Install voicemeeter-api package from your console
|
|
||||||
|
|
||||||
`pip install voicemeeter-api`
|
`pip install voicemeeter-api`
|
||||||
|
|
||||||
## `Use`
|
## `Use`
|
||||||
@@ -55,12 +51,12 @@ class ManyThings:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def other_things(self):
|
def other_things(self):
|
||||||
|
self.vm.bus[3].gain = -6.3
|
||||||
|
self.vm.bus[4].eq = True
|
||||||
info = (
|
info = (
|
||||||
f"bus 3 gain has been set to {self.vm.bus[3].gain}",
|
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}",
|
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))
|
print("\n".join(info))
|
||||||
|
|
||||||
|
|
||||||
@@ -114,8 +110,6 @@ The following properties are available.
|
|||||||
- `limit`: int, from -40 to 12
|
- `limit`: int, from -40 to 12
|
||||||
- `A1 - A5`, `B1 - B3`: boolean
|
- `A1 - A5`, `B1 - B3`: boolean
|
||||||
- `label`: string
|
- `label`: string
|
||||||
- `device`: string
|
|
||||||
- `sr`: int
|
|
||||||
- `mc`: boolean
|
- `mc`: boolean
|
||||||
- `k`: int, from 0 to 4
|
- `k`: int, from 0 to 4
|
||||||
- `bass`: float, from -12.0 to 12.0
|
- `bass`: float, from -12.0 to 12.0
|
||||||
@@ -143,7 +137,7 @@ vm.strip[3].gain = 3.7
|
|||||||
print(vm.strip[0].label)
|
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
|
- `appgain(name, value)`: string, float, from 0.0 to 1.0
|
||||||
|
|
||||||
@@ -199,8 +193,6 @@ The following properties are available.
|
|||||||
- `sel`: boolean
|
- `sel`: boolean
|
||||||
- `gain`: float, from -60.0 to 12.0
|
- `gain`: float, from -60.0 to 12.0
|
||||||
- `label`: string
|
- `label`: string
|
||||||
- `device`: string
|
|
||||||
- `sr`: int
|
|
||||||
- `returnreverb`: float, from 0.0 to 10.0
|
- `returnreverb`: float, from 0.0 to 10.0
|
||||||
- `returndelay`: float, from 0.0 to 10.0
|
- `returndelay`: float, from 0.0 to 10.0
|
||||||
- `returnfx1`: 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)
|
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
|
### Macrobuttons
|
||||||
|
|
||||||
The following properties are available.
|
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.
|
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)`
|
`voicemeeterlib.api(kind_id: str)`
|
||||||
|
|
||||||
@@ -595,46 +676,6 @@ You may pass the following optional keyword arguments:
|
|||||||
- `midi`: midi updates
|
- `midi`: midi updates
|
||||||
- `ldirty`: level 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:
|
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.
|
- `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):
|
def other_things(self):
|
||||||
|
self.vm.bus[3].gain = -6.3
|
||||||
|
self.vm.bus[4].eq = True
|
||||||
info = (
|
info = (
|
||||||
f"bus 3 gain has been set to {self.vm.bus[3].gain}",
|
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}",
|
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))
|
print("\n".join(info))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import argparse
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
|
|
||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
@@ -13,6 +15,11 @@ from pyparsing import (
|
|||||||
nums,
|
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:
|
class Parser:
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
@@ -53,46 +60,39 @@ class Parser:
|
|||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
def main(cmds=None):
|
def interactive_mode(parser):
|
||||||
kind_id = "banana"
|
|
||||||
|
|
||||||
with voicemeeterlib.api(kind_id) as vm:
|
|
||||||
parser = Parser(vm)
|
|
||||||
if cmds:
|
|
||||||
res = parser.parse(cmds)
|
|
||||||
if res:
|
|
||||||
print(res)
|
|
||||||
else:
|
|
||||||
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
while cmd := input("Please enter command (Press <Enter> to exit)\n"):
|
||||||
if not cmd:
|
if not cmd:
|
||||||
break
|
break
|
||||||
res = parser.parse((cmd,))
|
if res := parser.parse((cmd,)):
|
||||||
if res:
|
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)
|
print(res)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
cmds = (
|
main()
|
||||||
"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)
|
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
import voicemeeterlib
|
import voicemeeterlib
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Observer:
|
class Observer:
|
||||||
def __init__(self, vm, midi_btn, macrobutton):
|
# leftmost M on korg nanokontrol2 in CC mode
|
||||||
self.vm = vm
|
MIDI_BUTTON = 48
|
||||||
self.midi_btn = midi_btn
|
MACROBUTTON = 0
|
||||||
self.macrobutton = macrobutton
|
|
||||||
|
|
||||||
def register(self):
|
def __init__(self, vm):
|
||||||
|
self.vm = vm
|
||||||
self.vm.subject.add(self)
|
self.vm.subject.add(self)
|
||||||
|
|
||||||
def on_update(self, subject):
|
def on_update(self, subject):
|
||||||
@@ -32,23 +36,24 @@ class Observer:
|
|||||||
"""
|
"""
|
||||||
if (
|
if (
|
||||||
max(self.vm.strip[3].levels.postfader) > -40
|
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(
|
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:
|
else:
|
||||||
self.vm.button[self.macrobutton].trigger = False
|
self.vm.button[self.MACROBUTTON].trigger = False
|
||||||
self.vm.button[self.macrobutton].state = False
|
self.vm.button[self.MACROBUTTON].state = False
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
kind_id = "banana"
|
||||||
|
|
||||||
# we only care about midi events here.
|
# 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:
|
with voicemeeterlib.api(kind_id, subs=subs) as vm:
|
||||||
obs = Observer(vm, midi_btn, macrobutton)
|
obs = Observer(vm)
|
||||||
obs.register()
|
|
||||||
|
|
||||||
while cmd := input("Press <Enter> to exit\n"):
|
while cmd := input("Press <Enter> to exit\n"):
|
||||||
if not cmd:
|
if not cmd:
|
||||||
@@ -56,9 +61,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
kind_id = "banana"
|
|
||||||
# leftmost M on korg nanokontrol2 in CC mode
|
|
||||||
midi_btn = 48
|
|
||||||
macrobutton = 0
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- [OBS Studio](https://obsproject.com/)
|
- [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/obsws-python)
|
||||||
- [OBS Python SDK for Websocket v5](https://github.com/aatikturk/obsstudio_sdk)
|
|
||||||
|
|
||||||
## About
|
## About
|
||||||
|
|
||||||
@@ -20,6 +19,6 @@ password = "mystrongpass"
|
|||||||
|
|
||||||
## Notes
|
## 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
|
import voicemeeterlib
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
def on_start():
|
|
||||||
vm.strip[0].mute = True
|
|
||||||
vm.strip[1].B1 = True
|
|
||||||
vm.strip[2].B2 = True
|
|
||||||
|
|
||||||
|
|
||||||
def on_brb():
|
class Observer:
|
||||||
vm.strip[7].fadeto(0, 500)
|
def __init__(self, vm):
|
||||||
vm.bus[0].mute = True
|
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():
|
def on_brb(self):
|
||||||
vm.apply(
|
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-0": {"mute": True},
|
||||||
"strip-1": {"mute": True, "B1": False},
|
"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():
|
def on_current_program_scene_changed(self, data):
|
||||||
vm.strip[0].mute = False
|
def fget(scene):
|
||||||
vm.strip[7].fadeto(-6, 500)
|
run = {
|
||||||
vm.strip[7].A3 = True
|
"START": self.on_start,
|
||||||
vm.vban.instream[0].on = True
|
"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
|
scene = data.scene_name
|
||||||
print(f"Switched to scene {scene}")
|
print(f"Switched to scene {scene}")
|
||||||
|
if fn := fget(scene):
|
||||||
match scene:
|
fn()
|
||||||
case "START":
|
|
||||||
on_start()
|
|
||||||
case "BRB":
|
|
||||||
on_brb()
|
|
||||||
case "END":
|
|
||||||
on_end()
|
|
||||||
case "LIVE":
|
|
||||||
on_live()
|
|
||||||
case _:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
def main():
|
||||||
with voicemeeterlib.api("potato") as vm:
|
subs = {ev: False for ev in ["pdirty", "mdirty", "midi"]}
|
||||||
cl = obs.EventClient()
|
with voicemeeterlib.api("potato", subs=subs) as vm:
|
||||||
cl.callback.register(on_current_program_scene_changed)
|
obs = Observer(vm)
|
||||||
|
|
||||||
while cmd := input("<Enter> to exit\n"):
|
while cmd := input("<Enter> to exit\n"):
|
||||||
if not cmd:
|
if not cmd:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ from setuptools import setup
|
|||||||
setup(
|
setup(
|
||||||
name="obs",
|
name="obs",
|
||||||
description="OBS Example",
|
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
|
import voicemeeterlib
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
|
|
||||||
class Observer:
|
class Observer:
|
||||||
def __init__(self, vm):
|
def __init__(self, vm):
|
||||||
self.vm = vm
|
self.vm = vm
|
||||||
# register your app as event observer
|
# register your app as event observer
|
||||||
self.vm.subject.add(self)
|
self.vm.subject.add(self)
|
||||||
# add level updates, since they are disabled by default.
|
# enable level updates, since they are disabled by default.
|
||||||
self.vm.event.add("ldirty")
|
self.vm.event.ldirty = True
|
||||||
|
|
||||||
# define an 'on_update' callback function to receive event updates
|
# define an 'on_update' callback function to receive event updates
|
||||||
def on_update(self, subject):
|
def on_update(self, subject):
|
||||||
@@ -16,22 +20,19 @@ class Observer:
|
|||||||
elif subject == "mdirty":
|
elif subject == "mdirty":
|
||||||
print("mdirty!")
|
print("mdirty!")
|
||||||
elif subject == "ldirty":
|
elif subject == "ldirty":
|
||||||
info = (
|
for bus in self.vm.bus:
|
||||||
f"[{self.vm.bus[0]} {self.vm.bus[0].levels.isdirty}]",
|
if bus.levels.isdirty:
|
||||||
f"[{self.vm.bus[1]} {self.vm.bus[1].levels.isdirty}]",
|
print(bus, bus.levels.all)
|
||||||
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))
|
|
||||||
elif subject == "midi":
|
elif subject == "midi":
|
||||||
current = self.vm.midi.current
|
current = self.vm.midi.current
|
||||||
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
|
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
kind_id = "banana"
|
||||||
|
|
||||||
with voicemeeterlib.api(kind_id) as vm:
|
with voicemeeterlib.api(kind_id) as vm:
|
||||||
obs = Observer(vm)
|
Observer(vm)
|
||||||
|
|
||||||
while cmd := input("Press <Enter> to exit\n"):
|
while cmd := input("Press <Enter> to exit\n"):
|
||||||
if not cmd:
|
if not cmd:
|
||||||
@@ -39,6 +40,4 @@ def main():
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
kind_id = "banana"
|
|
||||||
|
|
||||||
main()
|
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]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "22.1.0"
|
version = "22.1.0"
|
||||||
@@ -22,7 +14,7 @@ tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "black"
|
name = "black"
|
||||||
version = "22.6.0"
|
version = "22.8.0"
|
||||||
description = "The uncompromising code formatter."
|
description = "The uncompromising code formatter."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
@@ -33,6 +25,7 @@ click = ">=8.0.0"
|
|||||||
mypy-extensions = ">=0.4.3"
|
mypy-extensions = ">=0.4.3"
|
||||||
pathspec = ">=0.9.0"
|
pathspec = ">=0.9.0"
|
||||||
platformdirs = ">=2"
|
platformdirs = ">=2"
|
||||||
|
tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
colorama = ["colorama (>=0.4.3)"]
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
@@ -102,11 +95,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pathspec"
|
name = "pathspec"
|
||||||
version = "0.9.0"
|
version = "0.10.1"
|
||||||
description = "Utility library for gitignore style pattern matching of file paths."
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "platformdirs"
|
name = "platformdirs"
|
||||||
@@ -129,8 +122,8 @@ optional = false
|
|||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
dev = ["pre-commit", "tox"]
|
testing = ["pytest-benchmark", "pytest"]
|
||||||
testing = ["pytest", "pytest-benchmark"]
|
dev = ["tox", "pre-commit"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "py"
|
name = "py"
|
||||||
@@ -153,14 +146,13 @@ diagrams = ["railroad-diagrams", "jinja2"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest"
|
name = "pytest"
|
||||||
version = "7.1.2"
|
version = "7.1.3"
|
||||||
description = "pytest: simple powerful testing with Python"
|
description = "pytest: simple powerful testing with Python"
|
||||||
category = "dev"
|
category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
|
||||||
attrs = ">=19.2.0"
|
attrs = ">=19.2.0"
|
||||||
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
iniconfig = "*"
|
iniconfig = "*"
|
||||||
@@ -198,97 +190,30 @@ pytest = ">=3.6"
|
|||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
description = "A lil' TOML parser"
|
description = "A lil' TOML parser"
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.11"
|
python-versions = "^3.10"
|
||||||
content-hash = "13366a58ff2f3fa0de2cb1e3de2f66fff612610fa66bb909201ebaa434cce014"
|
content-hash = "9f887ae517ade09119bf1f2cf77261d2445ae95857b69470ce1707f9791ce080"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
atomicwrites = []
|
|
||||||
attrs = []
|
attrs = []
|
||||||
black = [
|
black = []
|
||||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"},
|
click = []
|
||||||
{file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"},
|
colorama = []
|
||||||
{file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"},
|
iniconfig = []
|
||||||
{file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"},
|
isort = []
|
||||||
{file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"},
|
mypy-extensions = []
|
||||||
{file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"},
|
packaging = []
|
||||||
{file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"},
|
pathspec = []
|
||||||
{file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"},
|
platformdirs = []
|
||||||
{file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"},
|
pluggy = []
|
||||||
{file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"},
|
py = []
|
||||||
{file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"},
|
pyparsing = []
|
||||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"},
|
pytest = []
|
||||||
{file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"},
|
pytest-randomly = []
|
||||||
{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"},
|
|
||||||
]
|
|
||||||
pytest-repeat = []
|
pytest-repeat = []
|
||||||
tomli = [
|
tomli = []
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "voicemeeter-api"
|
name = "voicemeeter-api"
|
||||||
version = "0.6.0"
|
version = "1.0.0"
|
||||||
description = "A Python wrapper for the Voiceemeter API"
|
description = "A Python wrapper for the Voiceemeter API"
|
||||||
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
authors = ["onyx-and-iris <code@onyxandiris.online>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -12,7 +12,8 @@ packages = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.11"
|
python = "^3.10"
|
||||||
|
tomli = { version = "^2.0.1", python = "<3.11" }
|
||||||
|
|
||||||
[tool.poetry.dev-dependencies]
|
[tool.poetry.dev-dependencies]
|
||||||
pytest = "^7.1.2"
|
pytest = "^7.1.2"
|
||||||
@@ -24,3 +25,10 @@ isort = "^5.10.1"
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=1.0.0"]
|
requires = ["poetry-core>=1.0.0"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
|
[tool.poetry.scripts]
|
||||||
|
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(
|
@pytest.mark.parametrize(
|
||||||
"index, param, value",
|
"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, "treble", -1.6),
|
||||||
(data.virt_in, "mid", 5.8),
|
(data.virt_in, "mid", 5.8),
|
||||||
(data.virt_in, "bass", -8.1),
|
(data.virt_in, "bass", -8.1),
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ from enum import IntEnum
|
|||||||
from math import log
|
from math import log
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
|
||||||
from .error import VMError
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .kinds import kinds_all
|
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 = IntEnum(
|
||||||
"BusModes",
|
"BusModes",
|
||||||
@@ -106,7 +105,7 @@ class Bus(IRemote):
|
|||||||
|
|
||||||
class PhysicalBus(Bus):
|
class PhysicalBus(Bus):
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, kind):
|
def make(cls, remote, i, kind):
|
||||||
"""
|
"""
|
||||||
Factory method for PhysicalBus.
|
Factory method for PhysicalBus.
|
||||||
|
|
||||||
@@ -116,18 +115,54 @@ class PhysicalBus(Bus):
|
|||||||
if kind.name == "potato":
|
if kind.name == "potato":
|
||||||
EFFECTS_cls = _make_effects_mixin()
|
EFFECTS_cls = _make_effects_mixin()
|
||||||
kls += (EFFECTS_cls,)
|
kls += (EFFECTS_cls,)
|
||||||
return type("PhysicalBus", kls, {})
|
return type(
|
||||||
|
"PhysicalBus",
|
||||||
|
kls,
|
||||||
|
{
|
||||||
|
"device": BusDevice.make(remote, i),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
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
|
@property
|
||||||
def device(self) -> str:
|
def identifier(self) -> str:
|
||||||
return self.getter("device.name", is_string=True)
|
return f"Bus[{self.index}].device"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.getter("name", is_string=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def sr(self) -> int:
|
def sr(self) -> int:
|
||||||
return int(self.getter("device.sr"))
|
return int(self.getter("sr"))
|
||||||
|
|
||||||
|
|
||||||
class VirtualBus(Bus):
|
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
|
Returns a physical or virtual bus subclass
|
||||||
"""
|
"""
|
||||||
BUS_cls = (
|
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()
|
BUSMODEMIXIN_cls = _make_bus_mode_mixin()
|
||||||
return type(
|
return type(
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import tomllib
|
try:
|
||||||
|
import tomllib
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
import tomli as tomllib
|
||||||
|
|
||||||
from .kinds import request_kind_map as kindmap
|
from .kinds import request_kind_map as kindmap
|
||||||
|
|
||||||
@@ -70,7 +74,6 @@ class TOMLStrBuilder:
|
|||||||
|
|
||||||
class TOMLDataExtractor:
|
class TOMLDataExtractor:
|
||||||
def __init__(self, file):
|
def __init__(self, file):
|
||||||
self._data = dict()
|
|
||||||
with open(file, "rb") as f:
|
with open(file, "rb") as f:
|
||||||
self._data = tomllib.load(f)
|
self._data = tomllib.load(f)
|
||||||
|
|
||||||
@@ -116,6 +119,8 @@ class Loader(metaclass=SingletonType):
|
|||||||
loads data into memory if not found
|
loads data into memory if not found
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger("config.Loader")
|
||||||
|
|
||||||
def __init__(self, kind):
|
def __init__(self, kind):
|
||||||
self._kind = kind
|
self._kind = kind
|
||||||
self._configs = dict()
|
self._configs = dict()
|
||||||
@@ -129,14 +134,16 @@ class Loader(metaclass=SingletonType):
|
|||||||
|
|
||||||
def parse(self, identifier, data):
|
def parse(self, identifier, data):
|
||||||
if identifier in self._configs:
|
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
|
return False
|
||||||
self.parser = dataextraction_factory(data)
|
self.parser = dataextraction_factory(data)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def register(self, identifier, data=None):
|
def register(self, identifier, data=None):
|
||||||
self._configs[identifier] = data if data else self.parser.data
|
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):
|
def deregister(self):
|
||||||
self._configs.clear()
|
self._configs.clear()
|
||||||
@@ -159,6 +166,7 @@ def loader(kind):
|
|||||||
|
|
||||||
returns configs loaded into memory
|
returns configs loaded into memory
|
||||||
"""
|
"""
|
||||||
|
logger = logging.getLogger("config.loader")
|
||||||
loader = Loader(kind)
|
loader = Loader(kind)
|
||||||
|
|
||||||
for path in (
|
for path in (
|
||||||
@@ -167,7 +175,7 @@ def loader(kind):
|
|||||||
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
Path.home() / "Documents/Voicemeeter" / "configs" / kind.name,
|
||||||
):
|
):
|
||||||
if path.is_dir():
|
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"):
|
for file in path.glob("*.toml"):
|
||||||
identifier = file.with_suffix("").stem
|
identifier = file.with_suffix("").stem
|
||||||
if loader.parse(identifier, file):
|
if loader.parse(identifier, file):
|
||||||
|
|||||||
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 abc import abstractmethod
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from functools import cached_property
|
from functools import cached_property
|
||||||
from typing import Iterable, NoReturn, Self
|
from typing import Iterable, NoReturn
|
||||||
|
|
||||||
from . import misc
|
from . import misc
|
||||||
from .base import Remote
|
|
||||||
from .bus import request_bus_obj as bus
|
from .bus import request_bus_obj as bus
|
||||||
from .command import Command
|
from .command import Command
|
||||||
from .config import request_config as configs
|
from .config import request_config as configs
|
||||||
@@ -13,6 +13,7 @@ from .kinds import KindMapClass
|
|||||||
from .kinds import request_kind_map as kindmap
|
from .kinds import request_kind_map as kindmap
|
||||||
from .macrobutton import MacroButton
|
from .macrobutton import MacroButton
|
||||||
from .recorder import Recorder
|
from .recorder import Recorder
|
||||||
|
from .remote import Remote
|
||||||
from .strip import request_strip_obj as strip
|
from .strip import request_strip_obj as strip
|
||||||
from .vban import request_vban_obj as vban
|
from .vban import request_vban_obj as vban
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ class FactoryBuilder:
|
|||||||
Separates construction from representation.
|
Separates construction from representation.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
logger = logging.getLogger("remote.factorybuilder")
|
||||||
BuilderProgress = IntEnum(
|
BuilderProgress = IntEnum(
|
||||||
"BuilderProgress",
|
"BuilderProgress",
|
||||||
"strip bus command macrobutton vban device option recorder patch fx",
|
"strip bus command macrobutton vban device option recorder patch fx",
|
||||||
@@ -49,51 +51,51 @@ class FactoryBuilder:
|
|||||||
def _pinfo(self, name: str) -> NoReturn:
|
def _pinfo(self, name: str) -> NoReturn:
|
||||||
"""prints progress status for each step"""
|
"""prints progress status for each step"""
|
||||||
name = name.split("_")[1]
|
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(
|
self._factory.strip = tuple(
|
||||||
strip(i < self.kind.phys_in, self._factory, i)
|
strip(i < self.kind.phys_in, self._factory, i)
|
||||||
for i in range(self.kind.num_strip)
|
for i in range(self.kind.num_strip)
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_bus(self) -> Self:
|
def make_bus(self):
|
||||||
self._factory.bus = tuple(
|
self._factory.bus = tuple(
|
||||||
bus(i < self.kind.phys_out, self._factory, i)
|
bus(i < self.kind.phys_out, self._factory, i)
|
||||||
for i in range(self.kind.num_bus)
|
for i in range(self.kind.num_bus)
|
||||||
)
|
)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_command(self) -> Self:
|
def make_command(self):
|
||||||
self._factory.command = Command.make(self._factory)
|
self._factory.command = Command.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_macrobutton(self) -> Self:
|
def make_macrobutton(self):
|
||||||
self._factory.button = tuple(MacroButton(self._factory, i) for i in range(80))
|
self._factory.button = tuple(MacroButton(self._factory, i) for i in range(80))
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_vban(self) -> Self:
|
def make_vban(self):
|
||||||
self._factory.vban = vban(self._factory)
|
self._factory.vban = vban(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_device(self) -> Self:
|
def make_device(self):
|
||||||
self._factory.device = Device.make(self._factory)
|
self._factory.device = Device.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_option(self) -> Self:
|
def make_option(self):
|
||||||
self._factory.option = misc.Option.make(self._factory)
|
self._factory.option = misc.Option.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_recorder(self) -> Self:
|
def make_recorder(self):
|
||||||
self._factory.recorder = Recorder.make(self._factory)
|
self._factory.recorder = Recorder.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_patch(self) -> Self:
|
def make_patch(self):
|
||||||
self._factory.patch = misc.Patch.make(self._factory)
|
self._factory.patch = misc.Patch.make(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def make_fx(self) -> Self:
|
def make_fx(self):
|
||||||
self._factory.fx = misc.FX(self._factory)
|
self._factory.fx = misc.FX(self._factory)
|
||||||
return self
|
return self
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import time
|
import time
|
||||||
from abc import ABCMeta, abstractmethod
|
from abc import ABCMeta, abstractmethod
|
||||||
from typing import Self
|
|
||||||
|
|
||||||
|
|
||||||
class IRemote(metaclass=ABCMeta):
|
class IRemote(metaclass=ABCMeta):
|
||||||
@@ -26,7 +25,7 @@ class IRemote(metaclass=ABCMeta):
|
|||||||
def identifier(self):
|
def identifier(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def apply(self, data: dict) -> Self:
|
def apply(self, data: dict):
|
||||||
def fget(attr, val):
|
def fget(attr, val):
|
||||||
if attr == "mode":
|
if attr == "mode":
|
||||||
return (getattr(self, attr), val, 1)
|
return (getattr(self, attr), val, 1)
|
||||||
|
|||||||
@@ -42,3 +42,12 @@ def bus_mode_prop(param):
|
|||||||
self.setter(param, 1 if val else 0)
|
self.setter(param, 1 if val else 0)
|
||||||
|
|
||||||
return property(fget, fset)
|
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):
|
def _set(self, key: int, velocity: int):
|
||||||
self.cache[key] = velocity
|
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 ctypes as ct
|
||||||
|
import logging
|
||||||
import time
|
import time
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from threading import Thread
|
from typing import Iterable, NoReturn, Optional, Union
|
||||||
from typing import Iterable, NoReturn, Optional, Self, Union
|
|
||||||
|
|
||||||
from .cbindings import CBindings
|
from .cbindings import CBindings
|
||||||
from .error import CAPIError, VMError
|
from .error import CAPIError, VMError
|
||||||
|
from .event import Event
|
||||||
from .inst import bits
|
from .inst import bits
|
||||||
from .kinds import KindId
|
from .kinds import KindId
|
||||||
from .misc import Event, Midi
|
from .misc import Midi
|
||||||
from .subject import Subject
|
from .subject import Subject
|
||||||
from .util import comp, grouper, polling, script
|
from .updater import Updater
|
||||||
|
from .util import grouper, polling, script
|
||||||
|
|
||||||
|
|
||||||
class Remote(CBindings):
|
class Remote(CBindings):
|
||||||
"""Base class responsible for wrapping the C Remote API"""
|
"""Base class responsible for wrapping the C Remote API"""
|
||||||
|
|
||||||
|
logger = logging.getLogger("remote.remote")
|
||||||
DELAY = 0.001
|
DELAY = 0.001
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
@@ -32,7 +35,7 @@ class Remote(CBindings):
|
|||||||
|
|
||||||
self.event = Event(self.subs)
|
self.event = Event(self.subs)
|
||||||
|
|
||||||
def __enter__(self) -> Self:
|
def __enter__(self):
|
||||||
"""setup procedures"""
|
"""setup procedures"""
|
||||||
self.login()
|
self.login()
|
||||||
self.init_thread()
|
self.init_thread()
|
||||||
@@ -46,37 +49,10 @@ class Remote(CBindings):
|
|||||||
def init_thread(self):
|
def init_thread(self):
|
||||||
"""Starts updates thread."""
|
"""Starts updates thread."""
|
||||||
self.running = True
|
self.running = True
|
||||||
print(f"Listening for {', '.join(self.event.get())} events")
|
self.event.info()
|
||||||
t = Thread(target=self._updates, daemon=True)
|
|
||||||
t.start()
|
|
||||||
|
|
||||||
def _updates(self):
|
self.updater = Updater(self)
|
||||||
"""
|
self.updater.start()
|
||||||
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)
|
|
||||||
|
|
||||||
def login(self) -> NoReturn:
|
def login(self) -> NoReturn:
|
||||||
"""Login to the API, initialize dirty parameters"""
|
"""Login to the API, initialize dirty parameters"""
|
||||||
@@ -85,7 +61,7 @@ class Remote(CBindings):
|
|||||||
self.run_voicemeeter(self.kind.name)
|
self.run_voicemeeter(self.kind.name)
|
||||||
elif res != 0:
|
elif res != 0:
|
||||||
raise CAPIError(f"VBVMR_Login returned {res}")
|
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()
|
self.clear_dirty()
|
||||||
|
|
||||||
def run_voicemeeter(self, kind_id: str) -> NoReturn:
|
def run_voicemeeter(self, kind_id: str) -> NoReturn:
|
||||||
@@ -242,7 +218,9 @@ class Remote(CBindings):
|
|||||||
buf = ct.create_string_buffer(1024)
|
buf = ct.create_string_buffer(1024)
|
||||||
res = self.vm_get_midi_message(ct.byref(buf), n)
|
res = self.vm_get_midi_message(ct.byref(buf), n)
|
||||||
if res > 0:
|
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:
|
for msg in vals:
|
||||||
ch, pitch, vel = msg
|
ch, pitch, vel = msg
|
||||||
if not self.midi._channel or self.midi._channel != ch:
|
if not self.midi._channel or self.midi._channel != ch:
|
||||||
@@ -287,9 +265,9 @@ class Remote(CBindings):
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
self.apply(self.configs[name])
|
self.apply(self.configs[name])
|
||||||
print(f"Profile '{name}' applied!")
|
self.logger.info(f"Profile '{name}' applied!")
|
||||||
except KeyError as e:
|
except KeyError as e:
|
||||||
print(("\n").join(error_msg))
|
self.logger.error(("\n").join(error_msg))
|
||||||
|
|
||||||
def logout(self) -> NoReturn:
|
def logout(self) -> NoReturn:
|
||||||
"""Wait for dirty parameters to clear, then logout of the API"""
|
"""Wait for dirty parameters to clear, then logout of the API"""
|
||||||
@@ -298,7 +276,7 @@ class Remote(CBindings):
|
|||||||
res = self.vm_logout()
|
res = self.vm_logout()
|
||||||
if res != 0:
|
if res != 0:
|
||||||
raise CAPIError(f"VBVMR_Logout returned {res}")
|
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):
|
def end_thread(self):
|
||||||
self.running = False
|
self.running = False
|
||||||
@@ -5,7 +5,7 @@ from typing import Union
|
|||||||
|
|
||||||
from .iremote import IRemote
|
from .iremote import IRemote
|
||||||
from .kinds import kinds_all
|
from .kinds import kinds_all
|
||||||
from .meta import bool_prop, float_prop
|
from .meta import bool_prop, device_prop, float_prop
|
||||||
|
|
||||||
|
|
||||||
class Strip(IRemote):
|
class Strip(IRemote):
|
||||||
@@ -82,14 +82,20 @@ class Strip(IRemote):
|
|||||||
|
|
||||||
class PhysicalStrip(Strip):
|
class PhysicalStrip(Strip):
|
||||||
@classmethod
|
@classmethod
|
||||||
def make(cls, kind):
|
def make(cls, remote, i, is_phys):
|
||||||
"""
|
"""
|
||||||
Factory method for PhysicalStrip.
|
Factory method for PhysicalStrip.
|
||||||
|
|
||||||
Returns a PhysicalStrip class.
|
Returns a PhysicalStrip class.
|
||||||
"""
|
"""
|
||||||
EFFECTS_cls = _make_effects_mixins[kind.name]
|
EFFECTS_cls = _make_effects_mixins(is_phys)[remote.kind.name]
|
||||||
return type(f"PhysicalStrip", (cls, EFFECTS_cls), {})
|
return type(
|
||||||
|
f"PhysicalStrip",
|
||||||
|
(cls, EFFECTS_cls),
|
||||||
|
{
|
||||||
|
"device": StripDevice.make(remote, i),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
return f"{type(self).__name__}{self.index}"
|
||||||
@@ -118,16 +124,60 @@ class PhysicalStrip(Strip):
|
|||||||
def audibility(self, val: float):
|
def audibility(self, val: float):
|
||||||
self.setter("audibility", val)
|
self.setter("audibility", val)
|
||||||
|
|
||||||
@property
|
|
||||||
def device(self):
|
class StripDevice(IRemote):
|
||||||
return self.getter("device.name", is_string=True)
|
@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
|
@property
|
||||||
def sr(self):
|
def identifier(self) -> str:
|
||||||
return int(self.getter("device.sr"))
|
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):
|
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):
|
def __str__(self):
|
||||||
return f"{type(self).__name__}{self.index}"
|
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"""
|
"""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)
|
**pan,
|
||||||
for param in [
|
**color,
|
||||||
"pan_x",
|
**fx,
|
||||||
"pan_y",
|
|
||||||
"color_x",
|
|
||||||
"color_y",
|
|
||||||
"fx_x",
|
|
||||||
"fx_y",
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
return type(
|
||||||
|
"XYVirt",
|
||||||
|
(),
|
||||||
|
{**pan},
|
||||||
|
)
|
||||||
|
|
||||||
FX_cls = type(
|
def _make_fx_cls():
|
||||||
|
if is_phys:
|
||||||
|
return type(
|
||||||
"FX",
|
"FX",
|
||||||
(),
|
(),
|
||||||
{
|
{
|
||||||
**{
|
**{
|
||||||
param: float_prop(param)
|
param: float_prop(param)
|
||||||
for param in [
|
for param in ["reverb", "delay", "fx1", "fx2"]
|
||||||
"reverb",
|
|
||||||
"delay",
|
|
||||||
"fx1",
|
|
||||||
"fx2",
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
**{
|
**{
|
||||||
f"post{param}": bool_prop(f"post{param}")
|
f"post{param}": bool_prop(f"post{param}")
|
||||||
@@ -341,13 +393,19 @@ def _make_effects_mixin(kind):
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
return type("FX", (), {})
|
||||||
|
|
||||||
if kind.name == "potato":
|
if kind.name == "basic":
|
||||||
return type(f"Effects{kind}", (XY_cls, FX_cls), {})
|
steps = (_make_xy_cls,)
|
||||||
return type(f"Effects{kind}", (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]:
|
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
|
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]
|
CHANNELOUTMIXIN_cls = _make_channelout_mixins[remote.kind.name]
|
||||||
|
|
||||||
_kls = (STRIP_cls, CHANNELOUTMIXIN_cls)
|
_kls = (STRIP_cls, CHANNELOUTMIXIN_cls)
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
import logging
|
||||||
|
|
||||||
|
|
||||||
class Subject:
|
class Subject:
|
||||||
"""Adds support for observers"""
|
"""Adds support for observers"""
|
||||||
|
|
||||||
|
logger = logging.getLogger("subject.subject")
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
"""list of current observers"""
|
"""list of current observers"""
|
||||||
|
|
||||||
@@ -22,16 +27,22 @@ class Subject:
|
|||||||
|
|
||||||
if observer not in self._observers:
|
if observer not in self._observers:
|
||||||
self._observers.append(observer)
|
self._observers.append(observer)
|
||||||
|
self.logger.info(f"{type(observer).__name__} added to event observers")
|
||||||
else:
|
else:
|
||||||
print(f"Failed to add: {observer}")
|
self.logger.error(
|
||||||
|
f"Failed to add {type(observer).__name__} to event observers"
|
||||||
|
)
|
||||||
|
|
||||||
def remove(self, observer):
|
def remove(self, observer):
|
||||||
"""removes an observer from _observers"""
|
"""removes an observer from _observers"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self._observers.remove(observer)
|
self._observers.remove(observer)
|
||||||
|
self.logger.info(f"{type(observer).__name__} removed from event observers")
|
||||||
except ValueError:
|
except ValueError:
|
||||||
print(f"Failed to remove: {observer}")
|
self.logger.error(
|
||||||
|
f"Failed to remove {type(observer).__name__} from event observers"
|
||||||
|
)
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
"""clears the _observers list"""
|
"""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