Dup Ver Goto 📝

ViewSheetMusic

PT2/lang/python/qt/pyside6/examples does not exist
To
232 lines, 705 words, 6827 chars Page 'ViewSheetMusic' does not exist.

This is a simple app that takes a bunch of pngs, each being a single line of sheet music, and displays some of them. It responds to OSC messages to advance one line, or one screen, and to toggle fullscreen. And there are keyboard shortcuts. It illustrates the use of QPixmap and QPainters, along with simple OSC control.

#!/usr/bin/env python3
help = """
Sheet music viewer.
Have individual lines in files. Either number these 1.png 2.png... and run in that directory,
or specify the line files, in order, on the command line.
If the first argument is numeric, it is taken as the initial number of lines per screen.

Keys:
  f      -- fullscreen
  space  -- advance one line
  pgdn   -- advance one screen
  pgup   -- go back one screen
  -/=    -- dec/inc number of lines per screen

OSC (port 4011):
  /f            -- fullscreen
  /next         -- next
  /pgdn         -- advance one screen
  /pgup         -- go back one screen
  /numlines x   -- if x is an int, set that numer of lines, if "+", inc, if "-" dec
  /goto n       -- goto line n
  /forward n    -- go forward n
  /back n       -- go back n
"""
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtNetwork import *
from glob import glob
from pythonosc.osc_message import OscMessage
import os
import sys
app = QApplication([])


class Sheet(QWidget):
  def __init__(self,*xs,pixmaps=None,num_lines=4,**kw):
    super().__init__(*xs,**kw)
    self.idx = 0
    self.pixmaps = pixmaps
    if pixmaps is None:
      raise ValueError("No pixmaps")
    self.select(self.idx)
    self.num_lines = num_lines
    self.full_screen = False
  def select(self,idx):
    self.idx = idx % len(self.pixmaps)
    self.update()
  def next(self):
    self.select(self.idx+1)
  def nextPage(self):
    self.select(self.idx+self.num_lines)
  def prevPage(self):
    self.select(self.idx-self.num_lines)
  def paintEvent(self,e):
    with QPainter(self) as qp:
      rect = self.rect()
      width = rect.width()
      height = rect.height()
      line_height = int(height/self.num_lines)
      font = QFont("Arial",24)
      metrics = QFontMetrics(font)
      qp.setFont(font)
      qp.fillRect(rect,QBrush(Qt.white))
      qp.setBrush(Qt.black)
      qp.setPen(QPen(Qt.black))
      for i in range(self.num_lines):
        y = i*line_height
        pidx = (self.idx + i) % len(self.pixmaps)
        pixmap = self.pixmaps[pidx].scaledToHeight(line_height,Qt.SmoothTransformation)
        pwidth = pixmap.width()
        dw = width - pwidth
        if dw < 80:
          pixmap = pixmap.scaledToWidth(width-80)
          pwidth = pixmap.width()
          dw = width - pwidth
        dw2 = dw//2
        qp.drawPixmap(dw2,y,pixmap)
        line_num = str(((self.idx+i)%len(self.pixmaps))+1)
        brect = metrics.boundingRect(line_num)
        bwidth = brect.width()
        bheight = brect.height()
        tx = dw2-5-bwidth
        ty = y + (line_height - bheight)//2 + metrics.ascent()
        qp.drawText(QPoint(tx,ty),line_num)

  def changeNumLines(self,n):
    if isinstance(n,str):
      if n.isnumeric():
        n = int(n)
      elif n == "+":
        n = self.num_lines+1
      elif n == "-":
        n = self.num_lines-1
    elif isinstance(n,float):
      n = int(n)
    n = min(6,max(2,n))
    self.num_lines = n
    self.update()
  def goto(self,n):
    try:
      n = int(n)
    except ValueError:
      print(f"Ignoring goto {n}")
      return
    self.select(n-1)
  def forward(self,n):
    try:
      n = self.idx + int(n)
    except ValueError:
      print(f"Ignoring goto {n}")
      return
    self.select(n)
  def back(self,n):
    try:
      n = self.idx - int(n)
    except ValueError:
      print(f"Ignoring goto {n}")
      return
    self.select(n)
  def keyPressEvent(self,e):
    print(e)
    k = e.key()
    if k == Qt.Key_F:
      self.toggleFullScreen()
    elif k == Qt.Key_Q:
      app.quit()
    elif k == Qt.Key_Space:
      self.select(self.idx+1)
    elif k == Qt.Key_PageUp:
      self.select(self.idx-self.num_lines)
    elif k == Qt.Key_PageDown:
      self.select(self.idx+self.num_lines)
    elif k == Qt.Key_Equal:
      self.num_lines = min(6,self.num_lines+1)
      self.update()
    elif k == Qt.Key_Minus:
      self.num_lines = max(2,self.num_lines-1)
      self.update()
    return super().keyPressEvent(e)
  def toggleFullScreen(self):
    self.full_screen = not self.full_screen
    if self.full_screen:
      self.setWindowState(self.windowState() | Qt.WindowFullScreen)
    else:
      self.setWindowState(self.windowState() & ~Qt.WindowFullScreen)


class Receiver(QObject):
  def __init__(self,target,port=4011):
    self.target = target
    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.lower() == "next":
      self.target.next()
    elif addr.lower() in ["f","fs","fullscreen"]:
      self.target.toggleFullScreen()
    elif addr.lower() in ["pgup","pageup"]:
      self.target.prevPage()
    elif addr.lower() in ["pgdn","pagedown"]:
      self.target.nextPage()
    elif addr.lower() == "goto":
      if len(params) == 1:
        self.target.goto(params[0])
      else:
        print(f"Wrong params to numlines")
    elif addr.lower() == "forward":
      if len(params) == 1:
        self.target.forward(params[0])
      else:
        print(f"Wrong params to numlines")
    elif addr.lower() == "back":
      if len(params) == 1:
        self.target.back(params[0])
      else:
        print(f"Wrong params to numlines")
    elif addr.lower() == "numlines":
      if len(params) == 1:
        self.target.changeNumLines(params[0])
      else:
        print(f"Wrong params to numlines")
    elif addr.lower() in ["pgdn","pagedown"]:
      self.target.nextPage()
    else:
      print(f"Unrecognised {addr=}")

args = sys.argv[1:]

num_lines = 4
if len(args) > 0:
  arg = args[0]
  if arg.isnumeric():
    num_lines = int(arg)
    num_lines = max(min(6,num_lines),2)
    args = args[1:]
if len(args) > 0:
  fns = args
else:
  i = 1
  fns = []
  while os.path.exists(fn:=f"{i}.png"):
    fns.append(fn)
    i += 1
pixmaps = [ QPixmap(fn) for fn in fns ]

sheet = Sheet(pixmaps=pixmaps,num_lines=num_lines)
sheet.resize(900,800)
sheet.show()

receiver = Receiver(sheet,4011)
exit(app.exec())