Very quick and dirty, but enough to show what key is being pressed.
import sys
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from rtmidi import MidiIn
args = sys.argv[1:]
if len(args) == 0:
pat = "Through Port-3"
else:
pat = args[0]
white_notes = [ 0, 2, 4, 5, 7, 9, 11 ]
black_notes = [ i for i in range(12) if not i in white_notes ]
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)
class K(QWidget):
def __init__(self,*xs,lo=25,hi=108,**kw):
super().__init__(*xs,**kw)
self.setRange(lo,hi)
self.noteson = set()
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))
#print(self.wrects)
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:
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)
app = QApplication([])
k = K()
k.resize(880,50)
k.show()
m = Midi(k)
exit(app.exec())
OSC
OSC Version
This listens for OSC messages of the form /on 45 and /off 75 rather than listening to a MIDI port.
This is so that the keyboard display works on devices over the network.
Midi Forward
This is the counterpart to the above keymon-osc.py. It listens to a midi port and forwards OSC messages.
For now it only handles noteon and noteoff, and disregards channel, since for the keyboard monitor example,
that is all that's required.