Dup Ver Goto 📝

Qt Key Sender

DAW/osc/examples osc python does not exist
To
172 lines, 498 words, 5383 chars Page 'QtKeySender' does not exist.

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()