The Envivo looks like this: ![Envivo DJ Controller](EnvivoDJController.jpg) 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. ```py #!/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) ```