The Envivo looks like this:

I'm not sure whether or not it appears as a generic MIDI device under Windows or Mac, like it does in Linux. With the Novation Nocturn, for example, it only appears as a generic MIDI device in Linux, so I use a spare Lenovo Thinkpad T420 to plug these devices into, and that then sends OSC messages via my LAN to my music workstation. For things that aren't timing sensitive like actually playing keys or turning knobs to control VST parameters, the latency is fine. For things that require low latency, I naturally don't use this system.
This uses the middle Scratch/Rev/Sync buttons to select between 6 banks, which for now just include the bank number in the OSC address when jog wheel or button presses are sent. The middle rotary knob doesn't bank-switch, so sends the same OSC no matter which bank is selected. I use the rotary to adjust the grid size, and the jog wheels in banks 0 and 1 advance/rewind the edit curser by beats and bars (bank 0) and samples and grid divisions (bank 1). I haven't mapped the buttons yet, but the mapping is entirely done inside Reaper: this scripts just generates unique OSC messages for each button/control. (Note that I haven't added code for the pots as I don't have a use for them yet, in part because while Reaper scripts can be triggered by OSC, they can't receive parameters, which makes absolute valued encoders like pots effectively useless.)
The general idea with the device/delegate split is that with these devices the relationship
between controls and CC numbers is pretty ad hoc, so we map them to serially indexed rot,
but, and other events, and convert from 7bit midi values to signed integers in the case
of rotaries, and 1/0 values for button down/up, and so on. Then the delegate is dealing with
events with a more sensible numbering and value system (e.g. knob 0 dx=+2, knob 1 dx=-1) than
the raw MIDI (e.g. CC 24 +7 for a singe clockwise movement of a jog wheel and CC 25 123 for
a single counterclockwise movement of the other jog wheel).
The basic pattern here is a simple hardware abstraction layer. There is more coupling than is ideal, for example when bank switching, the delegate looks up the CC for the bank switching numbers. Part of the issue here is whether bank switching is done inside the device or the delegate. Generally I just hack things together quickly so that it does enough for me and call it a day. So it's a bodge.
#!/usr/bin/env python3
import sys
from datetime import datetime
from rtmidi import MidiIn, MidiOut
from pythonosc import udp_client
if len(sys.argv) > 1:
target = sys.argv[1]
else:
target = "behemoth"
midiin = MidiIn()
midiout = MidiOut()
def get_midi_ports_matching(pat):
ins = []
outs = []
for i,x in enumerate(midiin.get_ports()):
if pat in x:
ins.append(i)
for i,x in enumerate(midiout.get_ports()):
if pat in x:
outs.append(i)
ports = list(zip(ins,outs))
if len(ports) == 0:
print(f"No ports matchng {pat}")
return ports
def now():
return datetime.now().strftime("%c")
envivo_buttons = {
# cc no: button name
#left
68: "left_pitch_dec",
67: "left_pitch_inc",
59: "left_cue",
74: "left_play",
#midleft
75: "left_load",
72: "left_scratch",
64: "left_rev",
51: "left_sync",
#mid
13: "mid_folder_out",
#midright
52: "right_load",
53: "right_scratch",
71: "right_rev",
60: "right_sync",
#right
70: "right_pitch_dec",
69: "right_pitch_inc",
66: "right_cue"
}
envivo_banks = {
# name: (cc no, bank no)
"left_scratch": (72,0),
"left_rev": (64,1),
"left_sync": (51,2),
"right_scratch": (53,3),
"right_rev": (71,4),
"right_sync": (60,5)
}
class Envivo:
"""Manages the rtmidi device, passes events onto the delegate,
and the delegate can use this instance to send messages to the hardware device.
Things like jog wheels and endless rotaries are converted to serially indexed delegate.rot() calls.
Likewise buttons are convereted to serially indexed delegate.rub() calls.
"""
def __init__(self,input_port,output_port,name,delegate = None):
self.input_port = input_port
self.name = name
self.midiin = MidiIn()
self.midiin.set_callback(self.handle_midi)
self.midiout = MidiOut()
self.delegate = delegate
try:
self.midiin.open_port(input_port)
except Exception:
print(f"#Fail open midi port in Envivo")
self.midiin = None
return
try:
self.midiout.open_port(output_port)
except Exception:
print(f"#Fail open midi port in Envivo")
self.midiout = None
return
self.delegate.set_device(self)
def deactivate(self):
if self.midiin is not None:
self.miniin.close_port()
def send_msg(self,status,a,b):
self.midiout.send_message([status,a,b])
def send_cc(self,n,v):
self.send_msg(0xB0,n,v)
def handle_midi(self,xs,y):
delegate = self.delegate
print(now(),xs,y)
msg,dt = xs
status = msg[0]
if status == 0xB0:
_, cc, val = msg
print("cc",cc,val)
if cc == 0x19:
# left jog
if val >= 0x40:
val -= 0x80
val = -1 if val < 0 else 1
if delegate:
delegate.rot(0,val)
elif cc == 0x18:
# right jog
if val >= 0x40:
val -= 0x80
val = -1 if val < 0 else 1
if delegate:
delegate.rot(1,val)
elif cc == 0x1A:
if val >= 0x40:
val -= 0x80
val = -1 if val < 0 else 1
if delegate:
delegate.rot(2,val)
elif status == 0x90:
_, n, v = msg
print("down",n,v)
delegate.but(n,1)
elif status == 0x80:
_, n, v = msg
print("up",n,v)
delegate.but(n,0)
class EnvivoReaperDelegate:
"""Handles translating controller events to OSC messages"""
def __init__(self,host="192.168.1.4",port=9000):
# make osc client
self.client = udp_client.SimpleUDPClient(host,port)
self.device = None
self.bank = 0
def set_device(self,device):
self.device = device
for name,xs in envivo_banks.items():
cc,bank = xs
device.send_cc(cc,127 if bank == self.bank else 0)
def rot(self,n,dx):
print("rot",n,dx)
if n == 0:
v = "dec" if dx < 0 else "inc"
self.client.send_message(addr := f"/rpr/envivo/leftjog/{self.bank}/{v}",())
print(addr)
elif n == 1:
v = "dec" if dx < 0 else "inc"
self.client.send_message(addr := f"/rpr/envivo/rightjog/{self.bank}/{v}",())
print(addr)
elif n == 2:
v = "dec" if dx < 0 else "inc"
self.client.send_message(addr := f"/rpr/envivo/midrot/{v}",())
print(addr)
def but(self,n,x):
print("rpr but",n,x)
if x > 0:
if n in envivo_buttons:
b = envivo_buttons[n]
if b in envivo_banks:
device = self.device
cc, self.bank = envivo_banks[b]
for name,xs in envivo_banks.items():
cc,bank = xs
device.send_cc(cc,127 if bank == self.bank else 0)
else:
self.client.send_message(addr := f"/rpr/envivo/button/{self.bank}/{b}",())
print(addr)
else:
print("ignore",n)
def setup_envivos(ios):
i,o = ios[0]
envivo = Envivo(i,o,f"Envivo {len(envivos)}",EnvivoReaperDelegate())
envivos.append(envivo)
envivos_ios = get_midi_ports_matching("MIDI 8")
envivos = []
setup_envivos(envivos_ios)
hhs = envivos
from time import sleep
try:
while True:
sleep(1)
except KeyboardInterrupt:
print("Ctrl-C")
for n in hhs:
n.delegate.deactivate()
exit(0)