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