There are two variants of this. One listens on a midi port (ordinarily a midi loopback that Reaper forwards incoming midi to), and an OSC version (so that there is then a midi intermediary that then forwards OSC messages to indicate note on and note off events). The purpose of this is simply so that I can see what keys I am pressing without looking directly at the keyboard (part of an attempt to learn to play the piano by tactile feel, spatial awareness and stuff, rather than having to look at my hands while I play). ## Midi monitor ```py import sys from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtNetwork import * from pythonosc.osc_message import OscMessage from rtmidi import MidiIn from keyboard_display import KeyboardDisplay args = sys.argv[1:] if len(args) == 0: pat = "qtkb" else: pat = args[0] class Midi: def __init__(self,kb,pat=pat): self.midi_in = MidiIn() self.kb = kb ports = self.midi_in.get_ports() for i,x in enumerate(ports): if pat in x: self.port = i break else: print(f"Can't find midi port matching '{pat}'") exit(1) print(f"Port {i=} {x=}") self.midi_in.set_callback(self) self.midi_in.open_port(self.port) def __del__(self): self.midi_in.close_port() def __call__(self,x,y): data, t = x print(f"recv {data=}") if len(data) > 0: status = data[0]&0xf0 if status == 0x90: # noteon a,p,v = data if v > 0: self.kb.on(p) else: self.kb.off(p) elif status == 0x80: # noteoff a,p,v = data self.kb.off(p) app = QApplication([]) k = KeyboardDisplay(ontop=True) k.resize(1200,100) k.show() m = Midi(k,pat=pat) exit(app.exec()) ``` ## OSC monitor ```py import sys from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * from PySide6.QtNetwork import * from pythonosc.osc_message import OscMessage from rtmidi import MidiIn from keyboard_display import KeyboardDisplay class Receiver(QObject): def __init__(self,kb,port=4010): self.kb = kb self.udpSocket = QUdpSocket() self.udpSocket.readyRead.connect(self.handleUdp) self.udpSocket.bind(QHostAddress.Any,port) def handleUdp(self): while self.udpSocket.hasPendingDatagrams(): datagram = self.udpSocket.receiveDatagram(4096) data = datagram.data().data() print(f"Recv {data=} {len(data)} bytes") try: message = OscMessage(data) except Exception as e: print(f"Failed to parse {e} : {data=}") return self.processMessage(message) def processMessage(self,message): addr = message.address params = message.params if addr == "on": p = int(params[0]) self.kb.on(p) elif addr == "off": p = int(params[0]) self.kb.off(p) else: print(f"Unrecognised {addr=}") app = QApplication([]) k = KeyboardDisplay(ontop=True) k.resize(1200,100) k.show() m = Receiver(k) exit(app.exec()) ``` ## Keyboard Display ```py from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * white_notes = [ 0, 2, 4, 5, 7, 9, 11 ] black_notes = [ i for i in range(12) if not i in white_notes ] class KeyboardDisplay(QWidget): def __init__(self,*xs,lo=21,hi=108,ontop=True,**kw): super().__init__(*xs,**kw) self.setRange(lo,hi) self.noteson = set() if ontop == True: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint ) def on(self,p): print(f"on {p=}") self.noteson.add(p) print(self.noteson) self.update() def off(self,p): print(f"off {p=}") if p in self.noteson: self.noteson.remove(p) print(self.noteson) self.update() def setRange(self,lo=21,hi=108): # expand range if needed so that top and bottom notes are white if lo % 12 in black_notes: lo -= 1 if hi % 12 in black_notes: hi += 1 if hi - lo < 4: raise ValueError("Keyboard range too small") self.lo = lo self.hi = hi nw = 0 nb = 0 for i in range(lo,hi+1): if i % 12 in white_notes: nw += 1 else: nb += 1 self.nw = nw self.nb = nb self.calcRects() def calcRects(self): self.wrects = [] self.brects = [] w = 1/self.nw r = 0 for i,p in enumerate(range(self.lo,self.hi+1)): if p % 12 in white_notes: self.wrects.append((p,r,r+w)) r += w else: self.brects.append((p,r-w/3,r+w/3)) def mousePressEvent(self,e): modifiers = e.modifiers() self.resizing = False if modifiers == Qt.ControlModifier: self.close() elif modifiers == Qt.ShiftModifier: self.resizing = True self.mpos = e.position() self.msize = self.size() else: self.mpos = e.position() def mouseMoveEvent(self,e): buttons = e.buttons() if buttons == Qt.LeftButton: dpos = e.position() - self.mpos dpos = dpos.toPoint() if self.resizing: newsize = QSize(self.msize) newsize.setWidth(newsize.width() + dpos.x()) newsize.setHeight(newsize.height() + dpos.y()) print(newsize,dpos) if newsize.height() < 100: return if newsize.width() < 100: return print(1234) self.resize(newsize) else: newpos = self.pos() + dpos self.move(newpos) def paintEvent(self,e): num_wnotes = self.nw rect = self.rect() w = rect.width() h = rect.height() bh = int(h * 3/5) # we need to identify exactly what rectangle is what. # since we need to light them up when played. with QPainter(self) as qp: qp.fillRect(rect,Qt.white) qp.setPen(QPen(Qt.black,2)) for nr in self.wrects: p,l,r = nr if p in self.noteson: qp.setBrush(Qt.red) else: if p == 60: qp.setBrush(QColor(255,255,120)) else: qp.setBrush(Qt.white) l *= w r *= w l = int(l) r = int(r) dx = r-l qp.drawRect(l,0,dx,h) for nr in self.brects: p,l,r = nr if p in self.noteson: qp.setBrush(Qt.green) else: qp.setBrush(Qt.black) l *= w r *= w l = int(l) r = int(r) dx = r-l qp.drawRect(l,0,dx,bh) ``` ## Midi forwarder This listens to a midi port and forwards on and off messages as OSC. ```py import sys import socket from time import sleep from pythonosc.udp_client import SimpleUDPClient from pythonosc.osc_message import OscMessage from rtmidi import MidiIn from keyboard_display import KeyboardDisplay args = sys.argv[1:] if len(args) == 0: pat = "qtkb" else: pat = args[0] hosts = ["behemoth","t420c","music1","t470s1"] class Midi: def __init__(self,kb,pat=pat): self.midi_in = MidiIn() self.kb = kb ports = self.midi_in.get_ports() for i,x in enumerate(ports): if pat in x: self.port = i break else: print(f"Can't find midi port matching '{pat}'") exit(1) print(f"Port {i=} {x=}") self.midi_in.set_callback(self) self.midi_in.open_port(self.port) def __del__(self): self.midi_in.close_port() def __call__(self,x,y): data, t = x print(f"recv {data=}") if len(data) > 0: status = data[0]&0xf0 if status == 0x90: # noteon a,p,v = data if v > 0: self.kb.on(p) else: self.kb.off(p) elif status == 0x80: # noteoff a,p,v = data self.kb.off(p) def host_to_ip(host): try: info = socket.getaddrinfo(host,0,family=socket.AF_INET,type=socket.SOCK_DGRAM) return info[0][4][0] except socket.gaierror: print(f"Failed to look up host {host}") return "" class OscFw: def __init__(self,hosts,port): self.hosts = [ host_to_ip(host) for host in hosts ] self.clients = [ SimpleUDPClient(host,port) for host in self.hosts ] print(self.hosts) self.port = port def on(self,p): print(f"ON") addr = "on" params = [p] for client in self.clients: client.send_message(addr,params) def off(self,p): print("OFF") addr = "off" params = [p] for client in self.clients: client.send_message(addr,params) fw = OscFw(hosts,4010) m = Midi(fw,pat=pat) try: while True: sleep(1) except KeyboardInterrupt: print(f"Ctrl-C") exit() ``` ## Keyboard Display ```py from PySide6.QtCore import * from PySide6.QtGui import * from PySide6.QtWidgets import * white_notes = [ 0, 2, 4, 5, 7, 9, 11 ] black_notes = [ i for i in range(12) if not i in white_notes ] class KeyboardDisplay(QWidget): def __init__(self,*xs,lo=21,hi=108,ontop=True,**kw): super().__init__(*xs,**kw) self.setRange(lo,hi) self.noteson = set() if ontop == True: self.setWindowFlags(self.windowFlags() | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint ) def on(self,p): print(f"on {p=}") self.noteson.add(p) print(self.noteson) self.update() def off(self,p): print(f"off {p=}") if p in self.noteson: self.noteson.remove(p) print(self.noteson) self.update() def setRange(self,lo=21,hi=108): # expand range if needed so that top and bottom notes are white if lo % 12 in black_notes: lo -= 1 if hi % 12 in black_notes: hi += 1 if hi - lo < 4: raise ValueError("Keyboard range too small") self.lo = lo self.hi = hi nw = 0 nb = 0 for i in range(lo,hi+1): if i % 12 in white_notes: nw += 1 else: nb += 1 self.nw = nw self.nb = nb self.calcRects() def calcRects(self): self.wrects = [] self.brects = [] w = 1/self.nw r = 0 for i,p in enumerate(range(self.lo,self.hi+1)): if p % 12 in white_notes: self.wrects.append((p,r,r+w)) r += w else: self.brects.append((p,r-w/3,r+w/3)) def mousePressEvent(self,e): modifiers = e.modifiers() self.resizing = False if modifiers == Qt.ControlModifier: self.close() elif modifiers == Qt.ShiftModifier: self.resizing = True self.mpos = e.position() self.msize = self.size() else: self.mpos = e.position() def mouseMoveEvent(self,e): buttons = e.buttons() if buttons == Qt.LeftButton: dpos = e.position() - self.mpos dpos = dpos.toPoint() if self.resizing: newsize = QSize(self.msize) newsize.setWidth(newsize.width() + dpos.x()) newsize.setHeight(newsize.height() + dpos.y()) print(newsize,dpos) if newsize.height() < 100: return if newsize.width() < 100: return print(1234) self.resize(newsize) else: newpos = self.pos() + dpos self.move(newpos) def paintEvent(self,e): num_wnotes = self.nw rect = self.rect() w = rect.width() h = rect.height() bh = int(h * 3/5) # we need to identify exactly what rectangle is what. # since we need to light them up when played. with QPainter(self) as qp: qp.fillRect(rect,Qt.white) qp.setPen(QPen(Qt.black,2)) for nr in self.wrects: p,l,r = nr if p in self.noteson: qp.setBrush(Qt.red) else: if p == 60: qp.setBrush(QColor(255,255,120)) else: qp.setBrush(Qt.white) l *= w r *= w l = int(l) r = int(r) dx = r-l qp.drawRect(l,0,dx,h) for nr in self.brects: p,l,r = nr if p in self.noteson: qp.setBrush(Qt.green) else: qp.setBrush(Qt.black) l *= w r *= w l = int(l) r = int(r) dx = r-l qp.drawRect(l,0,dx,bh) ```