This is a simple GUI app that forwards keypresses via OSC. My main use is to bind to actions in Reaper. I connect a keyboard to a spare machine, and run this app, and keypresses turn into OSC messages sent to Reaper, which can be bound to actions. (Enable OSC in control surfaces in preferences, and enable binding messages to actions.)
It needs Python modules PySide6 and python-osc installed. This uses the convenient
from PySide6... import * thing, but that causes issues with more complex apps whose
source is split into multiple files.
The Code
#!/usr/bin/env python
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtNetwork import *
from pythonosc import udp_client
from pythonosc.osc_message import OscMessage
import sys,os,socket,re
import argparse
default_host = "localhost"
default_port = 9000
try:
keysend_port = int(os.getenv("KEYSEND_PORT",9000))
except ValueError:
print("Invalid KEYSEND_PORT -- using default")
keysend_port = default_port
def get_ip_for_host(host,family=socket.AF_INET):
try:
addrinfo = socket.getaddrinfo(host,0,family=family,type=socket.SOCK_DGRAM)
if len(addrinfo) == 0:
print(f"No info for {host}",file=sys.stderr)
return None
return addrinfo[0][4][0]
except socket.gaierror as e:
print(f"getaddrinfo failed for {host} : {e}",file=sys.stderr)
return None
class Config:
def __init__(self):
self.host = os.getenv("KEYSEND_HOST","localhost")
try:
self.port = int(os.getenv("KEYSEND_PORT",9000))
except ValueError:
self.port = 9000
print("Invalid KEYSEND_PORT -- default value used:",self.port)
self.hostname = socket.gethostname()
self.initial_channel = os.getenv("KEYSEND_CHANNEL","f1")
config = Config()
class SendKey:
def __init__(self,host,port,chan=999):
self.host = host
self.port = port
self.ip = get_ip_for_host(self.host)
self.client = udp_client.SimpleUDPClient(self.ip,self.port)
self.setChannel(chan)
self.delegates = {}
def send(self,k):
if self.chan in self.delegates:
return self.delegates[self.chan].send(self,k)
addr = f"/hh/{self.chan}/key/{k}"
print("send",addr)
self.client.send_message(addr,[])
def send_message(self,addr,*args):
self.client.send_message(addr,args)
def setTarget(self,ip,port=None):
self.ip = ip
if port is not None:
self.port = port
print(f"New target {ip}:{port}")
self.client = udp_client.SimpleUDPClient(self.ip,self.port)
def setChannel(self,chan):
chan = str(chan).replace("/","")
if len(chan) == 0:
return
self.chan = chan
print(f"Channel now {chan}")
# load from config.json, or ENV, not command line args
class Win(QWidget):
def __init__(self,config=config):
super().__init__()
self.sendkey = SendKey(config.host,config.port,chan=config.initial_channel)
self.resize(640,400)
self.channel = config.initial_channel
self.last = ""
self.history_length = 5
self.lasts = []
def paintEvent(self,e):
with QPainter(self) as p:
rect = self.rect()
p.fillRect(rect,QColor("black"))
p.setPen(QColor("#f7f"))
font = QFont("Optima",36)
p.setFont(font)
p.drawText(QPoint(300,100),f"Ch: {self.channel}")
p.setPen(QColor("#ff7"))
p.drawText(QPoint(250,200),f"{self.last}")
p.setPen(QColor("#77f"))
p.setFont(QFont("Optima",18))
p.drawText(QPoint(10,300),f"{' '.join(self.lasts)}")
def keyPressEvent(self,event):
sendkey = self.sendkey
key = event.key()
key = QKeySequence(key).toString().lower()
print("key",key)
if len(key) == 0:
return
if not key in "abcdefghijklmnopqrstuvwxyz0123456789" and\
not key in ["left","right","up","down"] and\
not key in ["home","end","pgup","pgdown"] and\
not key in ["ins","del","esc","space"] and\
not (key[0] == "f" and key[1:].isnumeric()) and\
not key in "!\"£$%^&*()_+-=[]{};:'#@~,./<>?\\|`¬":
return
modifiers = event.modifiers()
if modifiers & Qt.ShiftModifier:
key = "S-"+key
if modifiers & Qt.MetaModifier:
key = "M-"+key
if modifiers & Qt.ControlModifier:
key = "C-"+key
if modifiers & Qt.AltModifier:
key = "A-"+key
if key == "A-C-q":
app.quit()
if re.match(r"[ACSM-]*f\d+$",key):
self.channel = key
self.update()
return sendkey.setChannel(key)
print(f"Key: {key} to {sendkey.chan}")
self.last = key
self.lasts.append(key)
self.lasts = self.lasts[-self.history_length:]
sendkey.send(key)
self.update()
def main():
parser = argparse.ArgumentParser(prog="kb2osc",
description="Keys to OSC sender",add_help=False)
parser.add_argument("--help",action="help")
parser.add_argument("-h","--host",required=False)
parser.add_argument("-p","--port",type=int,required=False)
parser.add_argument("-c","--channel",help="Initial channel",required=False)
ns = parser.parse_args()
if ns.channel is not None:
config.initial_channel = ns.channel
if ns.host is not None:
config.host = ns.host
if ns.port is not None:
config.port = ns.port
hostname = socket.gethostname()
app = QApplication([])
win = Win()
win.show()
app.exec()
if __name__ == "__main__":
main()