Dup Ver Goto 📝

MidiKeyboardMonitor

PT2/lang/python/qt/pyside6/examples does not exist
To
460 lines, 1340 words, 11916 chars Page 'MidiKeyboardMonitor' does not exist.

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)