music/daw/reaper/ RawControlSurfaces
When it comes to mapping Reaper actions to control surface actions, with no feedback, it seems easiest to just send messages of the form
nocturn/0/knob/4/cw
for example.
Novation Nocturn
So I have a simple python script with basic bank switching. Now the nocturn looks like this:
Now on Widows and Mac, this device is essentially obsolete: on Windows (and I think Mac), it only works with Novation's discontinued Automap software. In a time where we need to be conscious of e-waste, this is inexcusable: the device just sends midi data to the Novation driver, and that then forwards it to the Automap software, while hiding the device (in the sense that it doesn't appear as a Midi device). Thank heavens for Linux, where if you plug a Nocturn in, it appears as a generic midi device. So for me, I plug them into a Raspberry Pi which runs a python script (see below) that then sends midi to/from the nocturn and sends OSC to a specified UDP port. As such, this script is agnostic to the application: it sends information about what knob was turned, or what button pressed, and it is for the recipient to map this to the desired target. (This will be a recurring concept in my exploration: my philosophy is to keep things as raw and abstract as possible, for as long as possible).
So bank switching: The Nocturn has 16 buttons, 8 smooth rotary knobs, and 1 rough (i.e. stepped) rotary knob. Now I use four for of those buttons as a bank selector (and the button for the selected bank lights up). Then I have 12 buttons remaining. So each bank has 12 buttons, 8 smooth rotaries and 1 rough rotary. To allow for more than one Nocturn, each gets a numerical id. (Though I want it to be possible for banks to be either local to an individual Nocturn, or global across e.g. 4 of them, at my later choice.) It then sends messages of the form
nocturn/0/srotary/34/cw
nocturn/0/rrotary/3/cw
nocturn/0/button/2/down
and then in Reaper I can use the 'learn' facility in the Actions dialog to assign these where I like.
If I wanted to send Reaper-specific messages to Reaper, I can add an intermediary (either in the python script, or external to it), which receives these raw input event messages and translates them. For example, I intend to investigate either AutoHotKey, or perhaps a simple app (maybe in C# to learn that language) that receives OSC events and generates virtual keyboard events.
Example Code exhibit A
This is what I'm currently playing around with:
#!/usr/bin/env python
import time
import rtmidi
from rtmidi.midiutil import open_midiinput
from pythonosc import udp_client
# map:
"""
Buttons: 0x70..0x7F -- send nonzero to put LED on.
Smooth rotaries: 0x40..0x47 -- send CC to set LED ring
01 for clockwise, 7F for anticlockwise
touch: 0x60..67 for touched, 00 for not touched
Middle encoder: 0x4A
01 for clockwise, 7F for anticlockwise
"""
class Nocturn:
"""Bass class for talking to the Nocturn
subclass and override e.g. handle_button
"""
def __init__(self,in_port,out_port,nocturn_id=0):
self.midiin = rtmidi.MidiIn()
self.midiout = rtmidi.MidiOut()
self.midiin.set_callback(self.handle)
self.midiin.open_port(in_port)
self.midiout.open_port(out_port)
self.nocturn_id = nocturn_id
print(f"Opened midi {in_port=} and {out_port=}")
def handle(self,event,data):
"(([bytes],time),None)"
bs,t = event
if bs[0] == 0xB0:
return self.handle_cc(bs[1],bs[2])
print(f"{xs=} {kw=}")
def close(self):
self.midiin.close_port()
def button_n_to_ccno(self,n):
return n + 0x70
def button_ccno_to_n(self,ccno):
return ccno - 0x70
def smooth_rotary_n_to_ccno(self,n):
return n+0x40
def smooth_rotary_ccno_to_n(self,ccno):
return ccno - 0x40
def rough_rotary_ccno(self):
return 0x4a
def ccval_to_dx(self,dx):
if dx >= 0x40:
dx -= 0x80
return dx
def handle_cc(self,ccno,ccval):
if ccno >= 0x40 and ccno <= 0x47:
n = ccno - 0x40
dx = self.ccval_to_dx(ccval)
return self.handle_smooth_rotary(n,dx)
elif ccno == 0x4A:
dx = self.ccval_to_dx(ccval)
return self.handle_rough_rotary(dx)
elif ccno >= 0x70 and ccno <= 0x7F:
n = ccno - 0x70
return self.handle_button(n, ccval)
elif ccno >= 0x60 and ccno <= 0x67:
n = ccno - 0x60
return self.handle_smooth_rotary_touch(n,ccval)
elif ccno == 0x52:
return self.handle_rough_rotary_touch(ccval)
else:
print(f"unhandled {ccno=} {ccval=}")
def handle_button(self,n,v):
pass # implement in subclass
def handle_smooth_rotary(self,n,dx):
print(f"Smooth rotary {n=} {dx=}")
def handle_rough_rotary(self,dx):
print(f"Rough rotary {dx=}")
def handle_smooth_rotary_touch(self,n,x):
print(f"Smooth rotary touch {n=} {x=}")
def handle_rough_rotary_touch(self,x):
print(f"Smooth rotary touch {x=}")
def set_button(self,n,v):
ccno = self.button_n_to_ccno(n)
self.set_cc(ccno,v)
def set_smooth_knob(self,n,x):
ccno = self.smooth_rotary_n_to_ccno(n)
self.set_cc(ccno,x)
def set_rough_knob(self,x):
ccno = self.rough_rotary_ccno()
self.set_cc(ccno,x)
def set_cc(self,ccno,ccval):
msg = [ 0xB0, ccno, ccval ]
print(f"Sending {msg}")
self.midiout.send_message(msg)
class Dispatcher:
def dispatch(self,path,params):
print(f"Dispatch stub {path=} {params=}")
class NullDispatcher(Dispatcher):
def dispatch(self,path,params):
pass
class OSCDispatcher(Dispatcher):
def __init__(self,host,port):
self.host = host
self.port = port
self.client = udp_client.SimpleUDPClient(host,port)
def dispatch(self,path,params):
if type(path) is list or type(path) is tuple:
path = "/".join(path)
print(f"Sending {path=} {params=}")
self.client.send_message(path,params)
class Nocturn1OSC(Nocturn):
"""Simple bank-switching nocturn --
the first n_banks of buttons switch banks, and the other buttons
function as buttons. So for example, with n_banks == 4 (the default)
you get four banks each with 12 buttons and 8+1 rotary encoders.
This does not do anything with the state of the LEDs aside from the
ones that indicate which bank is selected."""
def __init__(self,
in_port,out_port,
nocturn_id=0,
n_banks=4,
dispatcher=NullDispatcher(),
rough_granularity=2, # my nocturn generates two cw events for each click
smooth_granularity=1): # can be used to turn down the sensitivity
if n_banks >= 16:
raise ValueError("Too many banks requested")
super().__init__(in_port,out_port,nocturn_id=nocturn_id)
self.n_banks = n_banks
self.n_buttons = 16 - n_banks
self.n_rotaries = 8
self.set_bank(0)
self.dispatcher = dispatcher
self.smooth_granularity = smooth_granularity
self.rough_granularity = rough_granularity
self.rough_pos = 0
self.smooth_pos = [0]*8
def __del__(self):
for i in range(self.n_banks):
self.set_cc(0x70+i,00)
def handle_button(self,n,v):
if n < self.n_banks:
if v > 0:
return self.set_bank(n)
# ignore button up
else:
return self.bank_switched_button(n-self.n_banks,v)
def dispatch(self,subpath,params=[]):
xs = ["nocturn",self.nocturn_id]
if type(subpath) is list:
xs += subpath
elif type(subpath) is tuple:
xs += list(subpath)
else:
xs.append(subpath)
xs = [str(s) for s in xs]
self.dispatcher.dispatch(xs,params)
def bank_switched_button(self,n,v):
n += self.bank * self.n_buttons
print(f"Button {n} {v=}")
s = "down" if v > 0 else "up"
self.dispatch(["button",n,s])
def bank_switched_smooth_rotary(self,n,dx):
n += self.bank * self.n_rotaries
print(f"Rotary {n} {dx=}")
while dx > 0:
self.dispatch(["srotary",n,"cw"])
dx -= 1
while dx < 0:
self.dispatch(["srotary",n,"ccw"])
dx += 1
def bank_switched_rough_rotary(self,dx):
n = self.bank
print(f"Rough Rotary {n} {dx=}")
while dx > 0:
self.dispatch(["rrotary",n,"cw"])
dx -= 1
while dx < 0:
self.dispatch(["rrotary",n,"ccw"])
dx += 1
def handle_smooth_rotary(self,n,dx):
print(f"Smooth rotary {n=} {dx=}")
self.smooth_pos[n] += dx
if self.smooth_pos[n] % self.smooth_granularity == 0:
return self.bank_switched_smooth_rotary(n,dx)
def handle_rough_rotary(self,dx):
print(f"Rough rotary {dx=}")
#self.set_bank(self.bank+dx)
self.rough_pos += dx
if self.rough_pos % self.rough_granularity == 0:
return self.bank_switched_rough_rotary(dx)
def handle_smooth_rotary_touch(self,n,x):
print(f"Smooth rotary touch {n=} {x=}")
def handle_rough_rotary_touch(self,x):
print(f"Rough rotary touch {x=}")
def set_bank(self,n):
n = n % self.n_banks # makes wrapping around when using rough rotary easier
self.bank = n
print(f"Bank is now {self.bank}")
for i in range(4):
self.set_button(i,127 if i == n else 0)
midiin = rtmidi.MidiIn()
midiout = rtmidi.MidiOut()
nocturn_in = None
nocturn_out = None
ports = midiin.get_ports()
print(f"Input ports: {ports=}")
nocturns_in = []
nocturns_out = []
for i,x in enumerate(ports):
if "Nocturn" in x:
nocturns_in.append(i)
if len(nocturns_in) == 0:
print(f"Didn't find input port")
exit(1)
ports = midiout.get_ports()
print(f"Output ports: {ports=}")
for i,x in enumerate(ports):
if "Nocturn" in x:
nocturns_out.append(i)
if len(nocturns_out) == 0:
print(f"Didn't find output port")
exit(1)
print(f"{nocturns_in=} {nocturns_out=}")
nocturn = Nocturn1OSC(nocturns_in[0],nocturns_out[0],dispatcher=OSCDispatcher("behemoth",9000))
while True:
try:
time.sleep(1)
except KeyboardInterrupt:
print(f"Ctrl-C")
exit(0)