add support for midi devices.

midi example added.

minor version bump
This commit is contained in:
onyx-and-iris 2022-07-24 14:38:16 +01:00
parent 43d4496378
commit 9d446ea17d
11 changed files with 166 additions and 3 deletions

View File

@ -11,6 +11,16 @@ Before any major/minor/patch bump all unit tests will be run to verify they pass
- [ ] - [ ]
## [0.5.0] - 2022-07-24
### Added
- Midi class added to misc.
- Midi added to observer notifications
- Midi example added.
- Midi section added to readme.
- Minor version bump.
## [0.4.0] - 2022-07-21 ## [0.4.0] - 2022-07-21
### Added ### Added

View File

@ -519,6 +519,25 @@ vm.option.delay[4].set(30)
i, from 0 up to 4. i, from 0 up to 4.
### Midi
The following properties are available:
- `channel`: int, returns the midi channel
- `current`: int, returns the current (or most recently pressed) key
The following methods are available:
- `get(key)`: int, returns most recent velocity value for a key
example:
```python
print(vm.midi.get(12))
```
get() may return None if no value for requested key in midi cache
### Multiple parameters ### Multiple parameters
- `apply` - `apply`

24
examples/midi/README.md Normal file
View File

@ -0,0 +1,24 @@
## About/Requirements
A simple demonstration showing how to use a midi controller with this API.
This script was written for and tested with a Korg NanoKontrol2 configured in CC mode.
In order to run this example script you will need to have setup Voicemeeter with a midi device in Menu->Midi Mapping.
## Use
The script launches Voicemeeter Banana version and assumes that is the version being tested (if it was already launched)
`get_info()` responds to any midi button press or midi slider movement and prints its' CC number and most recent value.
`on_midi_press()` should enable trigger mode for macrobutton 0 if peak level value for strip 3 exceeds -40 and midi button 48 is pressed. On the NanoKontrol2 midi button 48 corresponds to the leftmost M button. You may need to disable any Keyboard Shortcut assignment first.
For a clear illustration of what may be done fill in some commands in `Request for Button ON / Trigger IN` and `Request for Button OFF / Trigger OUT`.
## Resources
If you want to know how to setup the NanoKontrol2 for CC mode check the following resources.
- [Korg NanoKontrol2 Manual](https://www.korg.com/us/support/download/manual/0/159/1912/)
- [CC Mode Info](https://i.korg.com/uploads/Support/nanoKONTROL2_PG_E1_634479709631760000.pdf)

52
examples/midi/__main__.py Normal file
View File

@ -0,0 +1,52 @@
import voicemeeterlib
class Observer:
def __init__(self, vm, midi_btn, macrobutton):
self.vm = vm
self.midi_btn = midi_btn
self.macrobutton = macrobutton
def register(self):
self.vm.subject.add(self)
def on_update(self, subject):
if subject == "midi":
self.get_info()
self.on_midi_press()
def get_info(self):
current = self.vm.midi.current
print(f"Value of midi button {current} is {self.vm.midi.get(current)}")
def on_midi_press(self):
if (
max(self.vm.strip[3].levels.postfader) > -40
and self.vm.midi.get(self.midi_btn) != 0
):
print(
f"Strip 3 level is greater than -40 and midi button {self.midi_btn} is pressed"
)
self.vm.button[self.macrobutton].trigger = True
else:
self.vm.button[self.macrobutton].trigger = False
self.vm.button[self.macrobutton].state = False
def main():
with voicemeeterlib.api(kind_id) as vm:
obs = Observer(vm, midi_btn, macrobutton)
obs.register()
while cmd := input("Press <Enter> to exit\n"):
if not cmd:
break
if __name__ == "__main__":
kind_id = "banana"
# leftmost M on korg nanokontrol2 in CC mode
midi_btn = 48
macrobutton = 0
main()

View File

@ -19,6 +19,8 @@ class Observer:
f"[{self.vm.bus[4]} {self.vm.bus[4].levels.isdirty}]", f"[{self.vm.bus[4]} {self.vm.bus[4].levels.isdirty}]",
) )
print(" ".join(info)) print(" ".join(info))
if subject == "midi":
print(self.vm.midi.cache)
def main(): def main():

View File

@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "voicemeeter-api" name = "voicemeeter-api"
version = "0.4.0" version = "0.5.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"

View File

@ -9,8 +9,9 @@ from .cbindings import CBindings
from .error import CAPIError, VMError from .error import CAPIError, VMError
from .inst import bits from .inst import bits
from .kinds import KindId from .kinds import KindId
from .misc import Midi
from .subject import Subject from .subject import Subject
from .util import comp, polling, script from .util import comp, grouper, polling, script
class Remote(CBindings): class Remote(CBindings):
@ -20,6 +21,7 @@ class Remote(CBindings):
def __init__(self, **kwargs): def __init__(self, **kwargs):
self.cache = {} self.cache = {}
self.midi = Midi()
self.subject = Subject() self.subject = Subject()
self.strip_mode = 0 self.strip_mode = 0
self.running = None self.running = None
@ -69,6 +71,8 @@ class Remote(CBindings):
self.cache["strip_level"] = self._strip_buf self.cache["strip_level"] = self._strip_buf
self.cache["bus_level"] = self._bus_buf self.cache["bus_level"] = self._bus_buf
self.subject.notify("ldirty") self.subject.notify("ldirty")
if self.get_midi_message():
self.subject.notify("midi")
time.sleep(self.ratelimit) time.sleep(self.ratelimit)
@ -231,6 +235,22 @@ class Remote(CBindings):
), ),
) )
def get_midi_message(self):
n = ct.c_long(1024)
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))))
for msg in vals:
ch, pitch, vel = msg
if not self.midi._channel or self.midi._channel != ch:
self.midi._channel = ch
self.midi._most_recent = pitch
self.midi._set(pitch, vel)
return True
elif res == -1 or res == -2:
raise CAPIError(f"VBVMR_GetMidiMessage returned {res}")
@script @script
def sendtext(self, script: str): def sendtext(self, script: str):
"""Sets many parameters from a script""" """Sets many parameters from a script"""

View File

@ -99,6 +99,10 @@ class CBindings(metaclass=ABCMeta):
ct.POINTER(WCHAR * 256), ct.POINTER(WCHAR * 256),
] ]
vm_get_midi_message = libc.VBVMR_GetMidiMessage
vm_get_midi_message.restype = LONG
vm_get_midi_message.argtypes = [ct.POINTER(CHAR * 1024), LONG]
def call(self, func): def call(self, func):
res = func() res = func()
if res != 0: if res != 0:

View File

@ -1,3 +1,5 @@
from typing import Optional
from .error import VMError from .error import VMError
from .iremote import IRemote from .iremote import IRemote
from .kinds import kinds_all from .kinds import kinds_all
@ -227,3 +229,24 @@ class Delay(IRemote):
def set(self, val: int): def set(self, val: int):
self.setter(f"delay[{self.index}]", val) self.setter(f"delay[{self.index}]", val)
class Midi:
def __init__(self):
self._channel = None
self.cache = {}
self._most_recent = None
@property
def channel(self) -> int:
return self._channel
@property
def current(self) -> int:
return self._most_recent
def get(self, key: int) -> Optional[int]:
return self.cache.get(key)
def _set(self, key: int, velocity: int):
self.cache[key] = velocity

View File

@ -12,7 +12,7 @@ class Subject:
return self._observers return self._observers
def notify(self, modifier=None): def notify(self, modifier):
"""run callbacks on update""" """run callbacks on update"""
[o.on_update(modifier) for o in self._observers] [o.on_update(modifier) for o in self._observers]

View File

@ -1,4 +1,5 @@
import functools import functools
from itertools import zip_longest
from typing import Iterator from typing import Iterator
@ -61,3 +62,11 @@ def comp(t0: tuple, t1: tuple) -> Iterator[bool]:
""" """
for a, b in zip(t0, t1): for a, b in zip(t0, t1):
yield a == b yield a == b
def grouper(n, iterable, fillvalue=None):
"""
Group elements of an iterable by sets of n length
"""
args = [iter(iterable)] * n
return zip_longest(fillvalue=fillvalue, *args)