music/daw/reaper/ TourBox


So if I close the TourBox Console, I can access the TourBox as a simple usb-serial device. The only issue is finding the correct port. It seems to be COM5 on Windows, or /dev/ttyS4 under cygwin, and /dev/ttyACM? under Linux, and /dev/tty...something-weird-about-usb-modem on a mac. Once you know that, here's a simple script that turns tourbox events into OSC which you can target to an application. Obviously, if you are familiar with python, you can do things way more complicated than this. So I turn the knob, and it sends

tour/knob/cw

to wherever. I don't have bank switching like I do with my nocturn script. Though I could easily send

tour/0/0/knob/cw

instead, using the 0/0 bit to specify which bank of 'virtual tourboxes' the event is coming from. This bank switching will be a common thread in what I'm doing. The other issue is finding a decent way to indicate what is mapped to what button. In Reaper, the OSC bindings are stored in

%reaperres%/OSC/reaper-osc-actions.ini

where %reaperres% is your reaper resource folder (e.g. %APPDATA%/REAPER on Windows).

My first TourBox→OSC script

#!/usr/bin/env python
import serial # python -m pip install pyserial
import time
from pythonosc import udp_client # python -m pip install python-osc
import random
import time
import sys
import os
import platform
import socket


def get_ip():
  # workaround since reaper doesn't receive packets if sent to localhost rather than 192.168.0.x
  s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
  s.settimeout(0)
  try:
      # doesn't even have to be reachable
      s.connect(('10.254.254.254', 1))
      IP = s.getsockname()[0]
  except Exception:
      IP = '127.0.0.1'
  finally:
      s.close()
  return IP

port = os.getenv("port",os.getenv("PORT",9000))
try:
  port = int(port)
except ValueError:
  print(f"Port '{port}' must be an integer")
  exit(1)
host = os.getenv("host",os.getenv("HOST",get_ip()))

print(f"{host=} {port=}")

# xxd /dev/ttyACM3
def main():
  global client
  client = udp_client.SimpleUDPClient(host,port)
  com_port = os.getenv("com_port",None)
  if com_port is None:
    if platform.system() == "Linux":
      com_port = "/dev/ttyACM0"
    elif platform.system() == "Darwin":
      com_port = "weird"
    elif "cygwin" in platform.system().lower():
      com_port = "/dev/ttyS4"
    elif platform.system() == "Windows":
      com_port = "COM5:"
    else:
      print(f"Don't support {platform.system()}")
      exit(2)
  with serial.Serial(com_port,19200) as ser:
    try:
      while True:
        x = ser.read()
        y = ord(x)
        print(f"{y:02x}")
        handle(y)
    except KeyboardInterrupt:
      print(f"Ctrl-c")
      exit()

def button(n,dirn):
  if dirn == 0:
    action(f"button_{n:02x}")

def rotary(n,dirn):
  if dirn > 0:
    return
  action('rotary_{n:02x}_{dirn}')

def action(x,args=[]):
  print(x,args)
  client.send_message(f"tour/{x}",args)

knobs = {
  0x04: "knob",
  0x0f: "dial", 
  0x09: "vdial"
}
dpad = {
  0x10: "up",
  0x11: "down",
  0x12: "left",
  0x13: "right"
}
def handle(x):
  print(f"{x=}")
  dirn = x >> 7
  x &= 0x7F
  kdirn = x >> 4
  x &= 0x3f
  if x in knobs:
    if dirn > 0:
      return
    action(f"{knobs[x]}/"+("cw" if kdirn > 0 else "ccw"))
  elif x in dpad:
    if dirn > 0:
      return
    action(f"dpad/{dpad[x]}")
  else:
    if dirn > 0:
      return
    action(f"button/{x:02x}")

if __name__ == "__main__":
  main()